JavaScript 基础
"use strict"
"use strict" 可以明确地激活 ES5 新增的特性和一些被修改过的特性。
"use strict" 可以放在脚本最顶部。 放在脚本的最顶部,则整个文件都以现代模式工作。
变量
JS 中变量声明可以通过 var、let、const,
var 声明的变量存在变量提升,如下例子
function a() {
console.log(b) // undefined
var b = 1
console.log(b) // 1
}通过 let 声明的变量不存在提升。
function a() {
console.log(b) // Uncaught ReferenceError: Cannot access 'b' before initialization
let b = 1
console.log(b) // 1
}
a()在 if 等语句中通过 var 声明的变量可以在程序中的其他地方使用,例如:
if(true) {
var a = 10
}
function say() {
console.log(a) // 10
}通过 let 和 const 声明的变量则存在块级作用域的限制。
if(true) {
let a = 10
}
function say() {
console.log(a) // a is not defined
}通过 var 声明变量的话可以重复声明。而 let 和 const 不可以。
const 表示声明一个常量,而且要声明和初始化一起。否则会报错。
const 声明并初始化一个数组或对象,我们可以修改数组的元素或对象属性的值。这里的深层原因是 const 保留的是数组或对象的指针,仅仅是确保指针不变。
如何声明不可修改的对象呢?可以通过 Object.defineProperty()
const a = {}
Object.defineProperty(a, 'property1', {
value: 42,
writable: false
});
a.property1 = 1
console.log(a) // {property1: 42}类型
首先思考为什么编程语言中提供了类型这种抽象,类型帮助我们解决了什么问题?
- 类型解决了数据存的问题:通过不同的类型,编译器可以开辟不同的内存大小,解决数据如何存的问题。
- 类型解决了数据取的问题:由于内存中的数据本没有意义,而是通过人为的界定数据才有了实际的意义,比如内存中同样的 4 个字节,存放着同样的二进制码,如果这两块内存对应的两个变量类型不一样,那么它们意义就不一样了。
JS 中主要提供了如下类型:
- boolean
- number
- string
- null
- undefined
- symbol
- bigint
- object
在 JS 中 函数和正则都属于 object 类型,数组也属于 object 类型。
类型转换
转换算法是:
- 调用 objSymbol.toPrimitive 如果这个方法存在,
- 否则,如果 hint 是 "string"
- 尝试调用 obj.toString() 或 obj.valueOf(),无论哪个存在。
- 否则,如果 hint 是 "number" 或者 "default"
- 尝试调用 obj.valueOf() 或 obj.toString(),无论哪个存在。
null 和 undefined 的却别
- null 代表“无”、“空”或“值未知”的特殊值,undefined 表示未被赋值
- typeof null 返回 object,typeof undefined 返回 undefined
- null === null // true
- undefined === undefined // ture
- undefined === null // false
number 类型注意点
- 0.1 + 0.2 === 0.3 // false
- NaN == NaN // false
- NaN === Nan // false
NaN 表示不是一个数字,比如做显示类型转换的时候可能会得到这个结果。 Number(0/0)
数组的检测
Array.isArray()Object.prototype.toString.call([]),解释原理:
Object 类型
对象的创建
- 可通过对象字面量创建
- 可通过 new 关键字创建
- new 关键字的过程如下
- 一个新的空对象被创建并分配给 this。
- 函数体执行。通常它会修改 this,为其添加新的属性。
- 返回 this 的值。
- new 关键字的过程如下
- 可通过 Object.create() 创建,使用 Object.create(null) 创建原型为 null 的对象。相当于使用
{__proto__: null}
对象的继承
通过
Object.create()实现继承通过
prototype实现继承通过
mixin实现继承
__proto__ 和 prototype 的区别
__proto__用于设置对象的原型
for .. in 和 for ... of 区别
- for..in 会跳过 symbol
- for..in 会遍历出原型链上的属性
- for..of 用与可迭代对象,通过 for...of 和 Symbo.iterator 的配合,可以对 JS 中的很多数据结构进行统一的迭代处理
symbol
表示唯一标识,for..in 会跳过 symbol。Object.assign 会复制 symbol 属性。
symbol 可以在全局使用,利用 symbol.for(),此方法会查找全局 symbol 表,如果 symbol 不存在,则创建它。
// 从全局注册表中读取
let id = Symbol.for("id"); // 如果该 symbol 不存在,则创建它
// 再次读取(可能是在代码中的另一个位置)
let idAgain = Symbol.for("id");
// 相同的 symbol
alert( id === idAgain ); // trueJS 内部有很多"系统"层的 symbol,通过它们可以微调对象的各个方面:
Symbol.hasInstance
Symbol.isConcatSpreadable
Symbol.iterator
Symbol.toPrimitive
...Object.keys/values/entries 会忽略 symbol 属性
类数组
类数组是有索引和 length 属性的对象。类数组没有数组的一些方法,比如 pop、push。可以通过 Array.from 将其转为数组。
类数组也不能通过 for...of 遍历。
技术上来说,可迭代对象必须实现 Symbol.iterator 方法。 objSymbol.iterator 的结果被称为 迭代器(iterator)。由它处理进一步的迭代过程。 一个迭代器必须有 next() 方法,它返回一个 {done: Boolean, value: any} 对象,这里 done:true 表明迭代结束,否则 value 就是下一个值。 Symbol.iterator 方法会被 for..of 自动调用,但我们也可以直接调用它。 内建的可迭代对象例如字符串和数组,都实现了 Symbol.iterator。 字符串迭代器能够识别代理对(surrogate pair)。(译注:代理对也就是 UTF-16 扩展字符。)
Promise 和 async/await
在使用 promise 时候,为了防止代码抛出异常影响正常流程,需要增加 catch。
new Promise(function() {
noSuchFunction(); // 这里出现 error(没有这个函数)
}).then(() => {});
// VM5149:2 Uncaught (in promise) ReferenceError: noSuchFunction is not defined
try{
new Promise(function() {
noSuchFunction(); // 这里出现 error(没有这个函数)
}).then(() => {});
} catch (e) {
// 无法捕获 promise 中的错误
}可以对上面的代码增加 catch 方法:
new Promise(function() {
noSuchFunction(); // 这里出现 error(没有这个函数)
}).then(() => {}).catch(() => {});async/await 可以通过 try...catch 捕获异常
async function f() {
try {
let response = await fetch('http://no-such-url');
} catch (err) {
alert(err); // TypeError: failed to fetch
}
}
f()可迭代对象 和 generator
可迭代对象是数组的泛化,可迭代对象需要对象实现 Symbol.iterator 方法,并返回带有 next 方法的对象。
let range = {
from: 1,
to: 5
};
// 1. for..of 调用首先会调用这个:
range[Symbol.iterator] = function() {
// ……它返回迭代器对象(iterator object):
// 2. 接下来,for..of 仅与下面的迭代器对象一起工作,要求它提供下一个值
return {
current: this.from,
last: this.to,
// 3. next() 在 for..of 的每一轮循环迭代中被调用
next() {
// 4. 它将会返回 {done:.., value :...} 格式的对象
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
};
// 现在它可以运行了!
for (let num of range) {
alert(num); // 1, 然后是 2, 3, 4, 5
}递归
递归是一种强大的抽象,使得问题解决自然而然,而且代码量小。但是由于递归的执行机制,递归所使用的内存空间会比较大。好在递归基本上都可以转化为循环的实现,从而降低内存空间的占用。
// 非尾递归
function pow(x, n) {
if(n == 1) return x
return x * pow(x, n - 1)
}
// 尾递归
function pow(x, n) {
if(n == 1) return x
return pow(x * x, n - 1)
}
// 循环
function pow(x, n) {
while(n > 1) {
x = x * x
n = n - 1
}
return x
}不同的递归实现,其表现出来的执行时的形状是不一致的。尾递归很容易转换为循环。
递归的汇编实现?
Proxy 和 Reflect
事件循环
浏览器中 JS 的执行流程和 Node.js 中的执行流程都是基于事件循环的。 正因为事件循环才让 JS 变成了异步事件驱动的变成模型。
JS 事件通过事件队列实现,事件队列分为:宏任务队列和微任务队列
宏任务包括:鼠标事件,脚本加载事件、setTimeout 等,宏任务是更底层的机制。
微任务包括: Promise、async/await,微任务主要来自我们编写的代码。
每个宏任务之后,引擎会立即执行微任务队列中的所有任务,然后再执行其他的宏任务,或渲染,或进行其他任何操作。
setTimeout(() => console.log('timeout'))
Promise.resolve().then(() => console.log('promise'))
console.log('console')
// console
// promise
// timeout<object type="image/svg+xml" data="https://zh.javascript.info/article/event-loop/eventLoop-full.svg" width="407" height="391" class="image__image" data-use-theme=""> </object>
console.log(1);
setTimeout(() => console.log(2));
Promise.resolve().then(() => console.log(3));
Promise.resolve().then(() => setTimeout(() => console.log(4)));
Promise.resolve().then(() => console.log(5));
setTimeout(() => console.log(6));
console.log(7);
// 1
// 7
// 3
// 5
// 2
// 6
// 4事件冒泡和捕获
事件冒泡是从里向外,事件捕获是从外向里。通过设置 addEventListener 的参数可以进行控制。
事件委托是基于事件冒泡或事件捕获实现的,通过判断 event.target 是不是待处理的元素来实现
<div class="books">
<div class="book">1</div>
<div class="book">2</div>
</div>document.querySelector('.books').addEventListener('click', (e) => {
e.target.className === 'book' && console.log('selected')
})event.target 获取的是实际触发时间的元素,event.currentTarget 获取的是绑定此事件的元素
事件捕获很少在实际开发中应用,之前主要是因为事件捕获并未被大多数浏览器实现。
创建自定义事件
浏览器中的数据存储
HTML文档加载和资源加载
HTML 页面主要声明周期包括三个 DOMContentLoaded,load,beforeunload,unload
DOMContentLoaded 浏览器已完全加载 HTML,并构建了 DOM 树,但像 和样式表之类的外部资源可能尚未加载完成
load在页面中所有资源加载完毕后触发,包含图片资源,样式表。
DOMContentLoaded 事件 —— DOM 已经就绪,因此处理程序可以查找 DOM 节点,并初始化接口。
load 事件 —— 外部资源已加载完成,样式已被应用,图片大小也已知了。
beforeunload 事件 —— 用户正在离开:我们可以检查用户是否保存了更改,并询问他是否真的要离开。
unload 事件 —— 用户几乎已经离开了,但是我们仍然可以启动一些操作,例如发送统计数据。
script 的 defer 和 async
script 会阻塞 DOM 的构建,需要 script 执行完成才能继续 DOM 的构建。这会带来两个问题:
- 脚本不能访问它下面的 DOM 元素,因此,脚本无法给它们添加处理程序等。
- 如果脚本执行时间长,它会阻塞页面,用户可能看不到页面内容,这也是为什么推荐将脚本放在 body 的最后面的原因。
浏览器支持给 script 设置 defer 和 async 属性
defer 会延迟加载脚本,让 script 不阻塞 DOM 的构建,等 DOM 构建完成后带有 defer 的脚本才会执行。
- 具有 defer 特性的脚本不会阻塞页面。
- 具有 defer 特性的脚本总是要等到 DOM 解析完毕,但在 DOMContentLoaded 事件之前执行。
- 具有 defer 特性的脚本保持其相对顺序,就像常规脚本一样。
<script defer src="https://javascript.info/article/script-async-defer/long.js"></script>
<script defer src="https://javascript.info/article/script-async-defer/small.js"></script>- 如果 <script> 脚本没有 src,则会忽略 defer 特性。
async 特性意味着脚本是完全独立的:
- 浏览器不会因 async 脚本而阻塞(与 defer 类似)。
- 其他脚本不会等待 async 脚本加载完成,同样,async 脚本也不会等待其他脚本。
- DOMContentLoaded 和异步脚本不会彼此等待
- 异步脚本以“加载优先”的顺序执行。
字符串
JS 中的字符串和其他语言的字符串不一样,其他语言中的字符串用对象表示。
在 js 中,字符串分为字符串字面量和字符串对象,字符串字面量不能设置属性。字符串对象能设置属性。
const a = 'test string'
a.name = 'test name'
console.log(a.name)
const b = new String('test string')
b.name = 'test name'
console.log(b.name)在调用字符串字面量的方式入 substring 等方法时,会创建包装类型的对象,例如:a.substring(2)实际相当于执行如下:
const a = new String('test')
a.substring(2)
a = nullObject 构造函数是一个工厂方法,能根据传入的值返回相应的原始值包装类型的实例。
let obj = new Object('test')
console.log(obj instanceof String)
let b = new Object(true);
console.log(b instanceof Boolean)通过构造函数创建的数字、字符串、布尔值,都是 object 类型。
let str = new String('test')
typeof str // object
Object.prototype.toString.call(str) // '[object String]'
typeof 'test' // string字符串也是类数组,因为有下标和 length 属性。
单例内置对象
单例内置对象包含 Global 和 Math。Global 是 ECMA-262 规定的兜底对象。isNaN、isFinite、parseInt、parseFloat、encodeURI、encodeURIComponent 在 global 对象上。
global 对象上的属性:undefined\NaN\Infinity\Object\Array\Function\Boolean\String\Number\Date\RegExp\Symbol\Error\EvalError\RangeError\ReferenceError\SyntaxError\TypeError\URIError
encodeURI 不会编码 URL 组件的特殊字符,比如 冒号、斜杠、问号、井号
encodeURIComponent 会对所有字符进行编码。
eval 所执行的代码和 eval 调用的代码共享作用域。
eval("let msg = 'test'")
console.log(msg)数组空位
可以使用一串逗号创建空位。ES6 遍历这种数组,会将值当 undefined。ES6 之前的方法则会忽略这个空位,但具体行为也会因方法而异。
const arr = [1,2]
arr.length = 1
for(const a of arr) {
console.log(a)
}
// 1ES6 新增方法: fill、copyWithin
对象池的实现
正则表达式
全局匹配 vs 多行匹配
g:全局模式,表示查找字符串的全部内容,而不是找到第一个匹配的内容就结束。 m:多行模式,表示查找到一行文本末尾时会继续查找。