什么是原型链?

定义的函数、类或对象会自动创建原型对象,并通过prototypeconstructor互相指向对方,原型之间通过__proto__构成原型链,实例调用某个方法时,会沿原型链寻找该方法,直到找到指定方法,或遍历完整个原型链后抛出异常;

借助单链表理解:针对实例调用某方法的角度来讲,原型链可以看作一个链表,每个节点都是原型对象,以__proto__属性指向下一节点,头节点是实例本身,尾节点是Object.prototype

不同情况下的__proto__如下:

  • function 函数,箭头函数,其__proto__一定是Function.prototype,包括Function
  • 实例的__proto__一定是其构造函数的原型;
  • 原型的__proto__一定是Object.prototype

举个例子,画出下列代码执行过程中的所有相关对象,如下:

1
function Person() {} // or like this: class Person {}

原型链.png

箭头函数与 function 函数

“An ArrowFunction does not define local bindings for arguments, super, this, or new.target. “ —— ECMAScript Language Specification

  • 没有 new.target,不能通过 new 实例化,也没有 super;
  • 没有 prototype,不能通过 Object.create(prototype)实例化(虽然可以将参数设为箭头函数本身,但无意义,相当于箭头函数做返回值的原型,返回值的父级原型是 Function.prototype,而非 Object.prototype,);
  • 没有 arguments;
  • 不能通过 call、apply、bind 改变 this 指向;

注:非箭头函数,其 new.target 指向函数原型的 constructor;

什么是属性描述符(property descriptor)

属性描述符是属性的配置,分为:

  • 数据描述符(configurable、enumerable、value、writable)
  • 存取描述符(configurable、enumerable、set、get)

configurable 标识描述符是否可配置,即属性是否可以通过 delete 删除,描述符是否可修改,默认为 false,即不可再次修改描述符,且不能删除属性;

enumerable 标识属性是否可枚举,默认为 false;

value 标识属性值,默认为 undefined;

writable 标识 value 是否可以修改,默认为 false,即不可再次修改属性值;

get/set 标识属性的 getter/setter 函数,默认为 undefined;

什么是闭包?

简单回答:读取其他函数内部变量的函数;

详细回答:从现象上看,对于一个返回函数的函数,其第一次执行后,内部定义的变量将只能被返回的函数访问,这就形成了一个闭包;

作用:

  • 闭包内的变量无法被外部访问,可以实现私有属性;
  • for 循环内,闭包及变量提升导致的回调函数无法正确捕捉到自增变量:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 修改前
for (var i = 0; i < 10; i++) {
document.getElementById(i).onClick = function () {
alert(i); // 所有元素点击后都提示9
};
}

// 借助匿名自执行函数实现的双重闭包技巧修改后
for (var i = 0; i < 10; i++) {
document.getElementById(i).onClick = (function (msg) {
return function () {
alert(msg); // 正确显示各自id
};
})(i);
}

注:闭包需要维护一个词法作用域,将带来额外的性能开销,谨慎使用;

JS 有几种数据类型?

JS 是一种动态类型(变量可以动态赋予任意类型的值)、弱类型(不检查类型一致性,允许隐式类型转换)语言;

原始类型有七种:

  1. null,typeof null 显示 object
  2. undefined;
  3. boolean;
  4. number(Infinity, NaN);
  5. bigint;
  6. string;
  7. symbol;

引用类型有一种:

  1. object,typeof function () {}typeof class Person {}结果都为function,但函数实际上是 object 类型;

注:原始值就是语言底层的不可变值,共有七种类型,称为原始类型;

typeof 有几种结果

  1. typeof null is object
  2. typeof undefined is undefined
  3. typeof boolean is boolean
  4. typeof number is number
  5. typeof bigint is bigint
  6. typeof string is string
  7. typeof symbol is symbol
  8. typeof function is function

null 和 undefined 的区别

  • typeof null 结果为 object,typeof undefined 结果为 undefined;
  • null 是关键字,undefined 不是;
  • null 表示空对象或空指针,undefined 表示空值,用途不同:
    • 原型链的终点是 null;
    • 未赋值的变量或属性隐式初始化为 undefined,没有 return 语句的函数隐式返回 undefined;

instanceof 作用与模拟

instanceof 用于判断右值是否在左值的原型链上;

for in 与 for of

  • for in 遍历键(一般用于对象),for of 遍历值(一般用于数组);
  • for of 本质是遍历 iterator,被遍历的目标必须实现Symbol.iterator方法,for in 则不必;
  • for in 遍历自身及原型链上的可枚举属性名,顺序无法保证,for of 循环调用 iterator,顺序由 iterator 的实现决定;

Promise 用途与机制

Promise 用于解决 ES5 中的回调地狱问题,将层级嵌套结构改为链式结构,以提高代码可读性,降低维护成本;

Promise 本质上是采用观察者模式,通过 then 收集回调函数,监听的是 Promise 实例化时传入的 executor 函数,executor 在 Promise 实例化时执行,并且会获得两个参数 resolve 和 reject,通过执行 resolve/reject 来触发 then 收集的回调函数;

Promise 具有三种状态:pending,fulfilled,rejected;按照规范,只能由 pending 向 fulfilled 或 rejected 转化;

Promise 能够链式调用是由于 then 方法会返回一个新的 Promise 实例;

常用静态方法:

  • Promise.all(iterable)在 iterable 全部 fulfill 或其中一个 reject 时,返回一个 fulfilled 或 rejected Promise;
  • Promise.any(iterable),任意一个 fulfill 或全部 reject 时,返回 fulfilled 或 rejected Promise;
  • Promise.race(iterable),第一个 fulfill 或 reject 时,返回 fulfilled 或 rejected Promise;
  • Promise.resolve(value),返回 fulfilled Promise;
  • Promise.reject(error),返回 rejected Promise;

注:iterable 是指可迭代对象,如数组;

Generator Function 用途和机制

Generator Function 用 function*创建,执行后返回一个 Iterator,Iterator 的 next 方法用于执行被 yield 分割的函数体的代码,并返回一个对象,该对象的 value 属性就是 yield 右侧的值,done 标识迭代是否完成;

Generator Function 常见于执行异步操作,yield 交出控制权,next 交还控制权,并且 next 接受的参数会作为 yield 表达式的值,这都是执行异步操作的关键点;

因为需要把 next 方法传递给异步操作,以便完成后调用 next 交还控制权,异步操作的流程控制不太方便,代码也不直观;

yield*用于 Generator Function 内调用 Generator Function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function* a() {
yield 3;
yield 4;
}
function* b() {
yield 1;
yield 2;
yield* a();
}
// b 等同于 c
function* c() {
yield 1;
yield 2;
yield 3;
yield 4;
}

Async Function 用途和机制

Async Function 可以视为 Generator 函数与 Promise 的语法糖,内置实现了 Generator 的自动执行,并将 Generator 的最终执行结果封装进 Promise,下一步处理更方便;

await 可以看作 yield 的语法糖;

Async Function 与 Generator Function、Promise 的关系可以从伪造的代码中理解:

1
2
3
4
5
6
7
8
9
10
11
12
function boo() {
return new Promise((resolve) => {
setTimeout(() => {
resolve(11111);
}, 3000);
});
}
async function foo() {
const a = await boo();
return a;
}
foo().then((v) => console.log(v)); // 11111

相当于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function boo() {
return new Promise((resolve) => {
setTimeout(() => {
resolve(11111);
}, 3000);
});
}
function foo() {
return new Promise((resolve) => {
// 此处相当于Async Function的函数体
function* _foo() {
const a = yield boo;
return a;
}
// 自动执行Generator
function run(genFunc) {
const gen = genFunc();
function next(data) {
const result = gen.next(data);
if (result.done) {
resolve(result.value);
return;
}
// 注意这里!封装了then
result.value().then(next);
}
next();
}
run(_foo);
});
}
foo().then((v) => console.log(v)); // 11111

ES6 新特性汇总

  1. const,let
  2. 解构赋值
  3. 模板字符串
  4. 参数默认值,剩余参数
  5. Arrow Function
  6. Generator Function
  7. Async Function
  8. 新增数组方法:entries,keys,values,includes,from,find,findIndex 等
  9. 新增 Object 方法:assign,keys,values,entries 等
  10. 表达式定义的属性名
  11. Symbol
  12. Set,Map,WeakSet,WeakMap
  13. Promise
  14. Iterator

Iterator 用途和机制

Iterator 就是符合迭代器协议(Iterator protocol)的对象,实现了如下方法(必须实现 next):

  • next:用于遍历 Iterator 的一系列值,返回一个具有 value 和 done 属性的对象;
  • return:结束迭代;
  • throw:结束迭代并返回一个 Error 实例;

Symbol.Iterator 用途和机制

Symbol.Iterator 就是可迭代对象必须实现的一个 Symbol 类型的方法名,实现该方法的对象可以:

  • 通过 for…of 遍历;
  • 解构赋值;
  • 通过 Array.from(iterable)转化为数组;
  • 初始化 Set,Map;
  • 作为 Promise.all,Promise.race 的参数;
  • 通过 yield*把值导入到 Generator Function;

该方法名标识的方法要符合可迭代协议(Iterable protocol),该方法返回一个 Iterator;

显然,可以通过 Generator Function 实现该方法,因为 Generator Function 会返回一个 Iterator;

WeakSet 和 WeakMap

键只能存非空对象,且键与对象地址是一种弱引用关系,GC 机制不关注弱引用,所以在没有强引用(如通过变量指向一个对象地址)存在的情况下,弱引用的对象将被回收;

典型场景:保存 Dom 节点,被删除的节点会被 GC 正常回收,避免强引用导致内存泄漏;

Decorator

Decorator 是编译时执行的函数,目前提案处于 Stage 3(2023 年 7 月 5 日),但 ts 或 babel 等类库已经支持。

语法如下:

1
2
3
4
5
6
7
8
9
10
// tsconfig.json 中配置 compileOptions.experimentalDecorators = true

function myDecorator(target) {
target.prototype.description = "hello decorator";
}

@myDecorator
class Person() {}

console.log((new Person).description) // "hello decorator"

Proxy 和 Reflect

Proxy 用于代理目标对象的操作,例如读、写、添加属性、删除属性等,语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const target = {};

const handler = {
set(target, prop, value) {
// do something
target[prop] = value;
// do something
},
get(target, prop) {
// do something
return target[prop];
},
};

const proxy = new Proxy(target, handler);

Proxy 提供了元编程的能力,即针对编程的编程,基本可以看作是代理模式,代理属性读写(get、set、赋值、.)、delete、new、in 等;

Reflect 用于搭配 Proxy 使用,Reflect 有 Proxy 构造函数中的参数 handler 上的所有方法,通过 Reflect 执行这些方法等同于执行被代理对象的对应原生操作;

Proxy + Reflect 可以方便地实现对操作符的代理,改变默认行为或增加自定义行为,例如每次赋值时在控制台打印值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const target = {
name: "Neil",
};

const proxy = new Proxy(target, {
set(target, key, value) {
console.log(`${key} will be changed from ${target[key]} to ${value}`);
// 原生行为
const result = Reflect.set(target, key, value);
return result;
},
});

proxy.name = "Jack"; // print: name will be changed from Neil to Jack

细节:

  • Proxy 代理是在 Proxy 实例上,直接操作被代理对象 target 将不会触发代理行为;
  • const {proxy, revoke} = Proxy.revokable(target, handler),revoke 调用后将销毁 proxy 实例,后续调用将抛出错误;

class

class 用于定义类,细节如下:

  • 不提供 constructor 则将隐式地提供一个constructor() { return this; }
  • 类名指向其构造函数,typeof 结果为function
  • constructor 只能通过 new 指令调用,直接调用将抛出异常;
  • 依赖于 this 的方法必须定义为实例方法,通过箭头函数或 bind 绑定 this 指向;

原型方法是定义在 prototype 上方法,静态方法是定义在 constructor 上的方法,实例方法是定义在实例上的方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
name = "Neil";
// 实例方法
print1 = () => this.name;
// 原型方法
print2() {
return this.name;
}
}
// 静态方法
Person.print3 = function (name) {
return name;
};

class与实例之间的关系.jpg

0.1 + 0.2 = 0.30000000000000004

首先,十进制数在计算机底层只能以二进制形式存储:

  1. 整数部分转二进制:每次除 2 取余入栈,除数为 0 时顺序出栈;
  2. 小数部分转二进制:每次乘 2 取整入队,小数部分为 0 时顺序出队;

其次,js 中数字存储遵循 IEEE 754 标准——64 位双精度浮点数,1 符号位,11 位指数位,52 位尾数位,数字以科学计数法表示:符号 尾数 * 基数^指数

该问题的关键就在于尾数位是有限的(52 位),十进制小数经过上述转换后,尾数长度可能远超 52 位,此时就要舍去 52 位之后的二进制数,0.1 和 0.2 就是这样,它们在计算机中只是一个近似数,加运算实际是两个近似数之和,也就是0.30000000000000004.

解决方案:

  1. 先乘上 10 的倍数 x,把小数转换成整数,计算后结果除以 x 还原为小数;
  2. Number.prototype.toFixed(digits)指定计算结果的小数精度;
  3. Number.prototype.toPrecision(precision)指定计算结果的整体精度;
  4. 目前仍处于 stage 1 的Ecma TC39 JavaScript Decimal proposal,即显式十进制数运算(对应的,js 默认为隐式二进制数运算);