 JS 知识点
JS 知识点
  待补充的:Promise的使用、class的使用、JS设计模式
# 1 ES6,ES6 新特性
ES6 即 ECMAScript 6,也称 ES2015,是 2015 颁布的 JS 标准。ECMA 是一个国际化标准组织,JS 被提交到这里。
主要的新功能有:
- 新语法,如加入 let、const,扩展运算符和剩余参数...,解构,模板字面量,Symbol等
- 代码模块化,支持 export和import
- 数字、字符串、正则新特性,如提供十六进制、添加 Number.isNaN()(减少全局转换)、字符串includes()、repeat()函数等
- 对象、数组新特性,如对象支持重复属性、数组支持 Array.of()、Array.from()、引入fill()等新方法
- 函数新特性,如箭头函数、默认参数等
- 新增 Set和Map数据结构
- 新增迭代器和生成器,新增 for-of循环
- 支持 class直接生成类
- 新增 Promise
- 新增代理、反射
# 2 JS 数据类型
基本类型:number、boolean、string、null、undefined、symbol、bigint。
引用类型:object,注意 array、function 也是对象。
# 2.1 typeof
 可以使用 typeof 判断数据类型,它的返回值有:"number"、"boolean"、"string"、"undefined"、"symbol"、"bigint"、"object"、"function"。
- NaN、- Infinity的- typeof返回- "number"
- 未定义的变量做 typeof,不会报错,而返回"undefined"
- null的- typeof返回- "object",- array的- typeof返回- "object"(判断- array可用- Array.isArray()),基本类型的包装类对象的- typeof返回- "object"
- class的- typeof返回- "function"
对于函数、自定义类,需要用 instanceof 判断。
console.log(typeof 1);  // number
console.log(typeof Infinity); // number
console.log(typeof NaN);  // number
console.log(typeof true); // boolean
console.log(typeof "1");  // string
console.log(typeof undefined);  // undefined
console.log(typeof x);  // undefined,未定义变量
console.log(typeof null); // object
console.log(typeof Symbol("a"));  // symbol
console.log(typeof 1n); // bigint
console.log(typeof {}); // object
console.log(typeof []); // object,数组也是对象
console.log(typeof new Number(1));  // object,复杂原始类型
console.log(typeof function() {});  // function
console.log(typeof class A {}); // function,类也是函数
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 2.2 instanceof
 instanceof 判断复杂原始类型、object、自定义数据类型。
- 简单原始类型只能用 typeof,复杂原始类型只能用instanceof
- function用- typeof、- instanceof均可
- 对象、自定义类型用 typeof均返回"object",用instanceof可判断原型
console.log(typeof 1);  // number
console.log(1 instanceof Number); // false
console.log(typeof new Number(1));  // object
console.log(new Number(1) instanceof Number); // true
console.log(typeof /a/);  // object,正则表达式也是对象
console.log(/a/ instanceof RegExp); // true
console.log(typeof {});  // object
console.log({} instanceof Object); // true
console.log(typeof []);  // object
console.log([] instanceof Array); // true
function Teacher() {}
function Student() {}
const teacher = new Teacher();
console.log(typeof teacher);  // object
console.log(teacher instanceof Teacher); // true
console.log(teacher instanceof Student); // false
console.log(teacher instanceof Object); // true,teacher -> Teacher.prototype -> Object.prototype
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 2.3 手写 instanceof
 循环使用 __proto__ 或 Object.getPrototypeOf()。
function instanceOf(a, b) {
  if (typeof a !== 'object' && typeof a !== 'function') {
    return false; // 确保a不是基本类型
  }
  a = a ? a.__proto__ : null;
  while (true) {
    if (a === null) {
      return false;
    }
    if (a === b.prototype) {
      return true;
    }
    a = a.__proto__;
  }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 2.4 Object.prototype.toString.call()
 将 Object.prototype.toString() 用于其他变量,可以得到形如 "[object xxx]" 的字符串。
console.log(Object.prototype.toString.call(1)); // [object Number]
console.log(Object.prototype.toString.call(true));  // [object Boolean]
console.log(Object.prototype.toString.call("1")); // [object String]
console.log(Object.prototype.toString.call(null)); // [object Null]
console.log(Object.prototype.toString.call(undefined)); // [object Undefined]
console.log(Object.prototype.toString.call(Symbol("a"))); // [object Symbol]
console.log(Object.prototype.toString.call(1n));  // [object BigInt]
console.log(Object.prototype.toString.call([]));  // [object Array]
console.log(Object.prototype.toString.call({}));  // [object Object]
console.log(Object.prototype.toString.call(function() {})); // [object Function]
2
3
4
5
6
7
8
9
10
# 2.5 constructor
 由于对象的原型(即 __proto__ 属性指向的对象)有一个 constructor 属性指向构造函数,因此对象也可以调用它。基本类型也可以调用。
console.log((1).constructor === Number);  // true
console.log(true.constructor === Boolean);  // true
console.log("1".constructor === String);  // true
// console.log(null.constructor); // TypeError: Cannot read property 'constructor' of null
// console.log(undefined.constructor);  // TypeError: Cannot read property 'constructor' of undefined
console.log(Symbol("a").constructor === Symbol);  // true
console.log(1n.constructor === BigInt);  // true
console.log([].constructor === Array);  // true
console.log({}.constructor === Object); // true
console.log(function() {}.constructor === Function);  // true
console.log(new Date().constructor === Date); // true
console.log(/a/.constructor === RegExp);  // true
class Teacher {}
console.log(new Teacher().constructor === Teacher); // true
2
3
4
5
6
7
8
9
10
11
12
13
14
# 3 变量声明、变量提升、作用域
# 3.1 var、let、const 的区别
 const 除定义外不可重新赋值、定义时必须给定初始值外,和 let 并无不同。
注意 var 的作用域是 函数作用域,let、const 的作用域是 块作用域。
- 声明提升: var定义的变量会被提升到代码最顶端(赋值不提升),let不会提升(实际上会提升并形成暂时性死区)
- 重复声明: var定义的变量可以重复声明(甚至可以不加var声明,这时会突破函数作用域成为全局变量),let不允许重复声明(不同作用域下可以)
- 全局覆盖: var定义的变量会成为全局变量及全局对象的属性,let不会覆盖全局对象的属性
在 循环 中,let 不会出现异步回调问题,因为会创建同名变量;const 常用于 for-in。
几个细节:
- var定义的变量函数外是否可访问? 不可访问,- var是函数作用域。
- var定义的变量与- function声明的函数谁先提升? 函数优于变量提升,同名变量提升不会覆盖函数提升,函数声明提升而表达式不提升。
- 如何证明 - let声明的变量也进行了提升? 在函数中先使用全局- let变量,再声明同名- let变量,会发现不可访问,即形成暂时性死区。
# 3.2 setTimeout 中的 var 和 let
 - var:- setTimeout回调会在循环结束后再执行,输出- var变量的最终值
- let:每次遍历时- i都创建了同名变量,输出每次- let变量的循环值
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i)); // 3 3 3
}
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j)); // 0 1 2
}
var x = 0;
for (let y = 0; x < 3 && y < 3; x++, y++) {
  setTimeout(() => console.log(`${x},${y}`));  // 3,0 3,1 3,2
}
2
3
4
5
6
7
8
9
10
# 3.3 变量提升、函数声明提升
所有带 var 声明都会被提升到 函数作用域 最顶端;重复声明的变量只提升一次;非 var 声明的全局变量不提升。
函数声明也提升且优于变量提升;与函数同名的变量提升不覆盖函数;函数表达式不会提升。
- 情况 1: - var a = true; fun(); function fun() { if (a) { var a = 10; } console.log(a); }1
 2
 3
 4
 5
 6
 7
 8- 等同于: - function fun() { var a; if (a) { a = 10; } console.log(a); // undefined } var a; a = true; fun();1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11- function fun()和- var a提升,- fun()内部的- var a也提升。
- 情况 2: - fun(); var fun = function () { console.log("var"); } fun(); function fun() { console.log("fun"); } fun();1
 2
 3
 4
 5
 6
 7
 8
 9- 等同于: - function fun() { console.log("fun"); } var fun; fun(); // fun fun = function () { console.log("var"); } fun(); // var fun(); // var1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11- function fun()和- var fun提升,- var fun不覆盖- function fun()。函数表达式被认为是赋值。
- 情况 3: - var a = 1; function fn(){ var a = 2; function a(){console.log(3);} return a; function a(){console.log(4);} } var b = fn(); console.log(b);1
 2
 3
 4
 5
 6
 7
 8
 9- 等同于: - function fn(){ function a(){console.log(3);} function a(){console.log(4);} var a; a = 2; return a; } var a; var b; a = 1; b = fn(); console.log(b); // 21
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14- function fn()、- var a、- var b依次提升,内部的两个- function a()和- var a依次提升。
# 3.4 作用域
全局作用域,函数作用域,块作用域(ES6 新增)。
var 在全局定义的变量为全局作用域,也成为全局对象的属性;不加 var 声明的变量会突破函数作用域成为全局变量(全局对象的属性)。
(function() {
  var a = b = 5;
})();
var c = 55;
console.log(window.c);  // 55
console.log(b); // 5
console.log(window.b);  // 5
console.log(a); // ReferenceError: a is not defined
2
3
4
5
6
7
8
let、const 定义的作用域为块级作用域,如 if{}、for{} 等,甚至单纯的 {}(对象字面量语句除外),同一块级作用域下变量不能重复声明。
(() => {
  let x, y
  try {
    throw new Error()
  } catch (x) {
    (x = 1), (y = 2)
    console.log(x)  // 1
  }
  console.log(x)  // undefined
  console.log(y)  // 2
})()
2
3
4
5
6
7
8
9
10
11
# 4 操作符
# 4.1 == 和 ===
 ==:基础类型执行数据转换(转为数值,null 和 undefined 不转),对象比较引用
===:比较值和数据类型
console.log(null == undefined); // true
console.log("NaN" == NaN); // false
console.log(5 == NaN); // false
console.log(NaN == NaN); // false
console.log(NaN != NaN); // true
console.log(false == 0); // true
console.log(true == 1); // true
console.log(true == 2); // false
console.log(true == "1"); // true
console.log(undefined == 0); // false
console.log(null == 0); // false
console.log("5" == 5); // true
2
3
4
5
6
7
8
9
10
11
12
# 4.2 +
 一元操作符可以作为 Number() 使用。
二元操作符注意只要有字符串,就执行字符串连接,否则强转为数字相加(Symbol 不可强转)。对于对象,调用 valueOf() 或 toString()。
console.log(+true); // 1
console.log(1 + 2 + '3'); // 33
console.log(NaN + 1); // NaN,NaN不参与运算
console.log(Infinity + Infinity, -Infinity + -Infinity, Infinity + -Infinity);  // Infinity, -Infinity, NaN,遵循数学规则
console.log(+0 + +0, -0 + -0, +0 + -0); // 0, -0, 0,遵循数学规则
console.log(true + undefined, true + null, false + undefined, false + null);  // NaN, 1, NaN, 0,强转为数字
console.log([] + [], {} + {}, [] + {}); // '', [object Object][object Object], [object Object],强转为字符串
console.log({} + 1);  // [object Object]1,调用toString方法
console.log({ valueOf: () => 1 } + 1);  // 2,调用valueOf方法
console.log({ toString: () => 1 } + 1); // 2,调用toString方法
2
3
4
5
6
7
8
9
10
# 5 数字
JS 中的数字是 64 位表示的。
# 5.1 Number 类和全局的 isNaN、isFinite 方法
 全局的 isNaN() 和 isFinite() 期待接收一个 number 参数,如果参数非 number 会被先强转为 number 类型。
Number 类的 isNaN() 和 isFinite() 为 ES6 新增,期待接收一个 unknown 参数,它首先会判断参数是否为 number 类型,如果不是直接返回 false,然后再判断该 number 类型是否是 NaN 或 Infinity。
const name = "John";
const age = 30;
const points = "100";
const nan = NaN;
const infinity = Infinity;
console.log(isNaN(name), Number.isNaN(name)); // true false
console.log(isNaN(age), Number.isNaN(age)); // false false
console.log(isNaN(points), Number.isNaN(points)); // false false
console.log(isNaN(nan), Number.isNaN(nan)); // true true
console.log(isNaN(infinity), Number.isNaN(infinity)); // false false
console.log(isFinite(name), Number.isFinite(name)); // false false
console.log(isFinite(age), Number.isFinite(age)); // true true
console.log(isFinite(points), Number.isFinite(points)); // true false
console.log(isFinite(nan), Number.isFinite(nan)); // false false
console.log(isFinite(infinity), Number.isFinite(infinity)); // false false
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 5.2 map 与 parseInt 问题
 以下代码输出什么?
console.log(["1", "2", "3"].map(parseInt)); // [1, NaN, NaN]
map 回调(及其他几个类似的函数)完整的参数列表为:value(当前值)、index(当前索引)、array(原数组)。
回调直接使用 parseInt 时,完整的参数列表会被传入 parseInt,例如第一次 map 传入 ("1", 0, ["1", "2", "3"])。
parseInt 最多可以接收两个参数,第一个参数是字符串,第二个参数表示进制(支持 2 到 36),会将字符串按对应的进制转换为十进制数字。当传入的进制为 0 时,默认为十进制。
因此,parseInt("1", 0) 返回 1,parseInt("2", 1) 返回 NaN,parseInt("3", 2) 返回 NaN(二进制不存在数字 3)。
如果想要实现预计的功能,可以使用 Number 代替 parseInt。
console.log(["0", "1", "2"].map(Number)); // [0, 1, 2]
# 6 字符串
# 6.1 实现一个模板字符串替换,数据源为对象
let template = "你好,我们是{{company}},我们来自{{group}},我们有{{business[0]}}、{{business[1]}}等。"
let obj = {
  company: "阿里",
  group: "蚂蚁",
  business: ["支付宝", "蚂蚁金服", "相互宝"]
}
2
3
4
5
6
7
先匹配所有的 {{*}} 字符串,然后逐一替换,可以,但不好。这里的正则表示:匹配形如 {{ 字母 [数字] }} 格式的字符串。
function render(template, obj) {
  const arr = template.match(/{{[a-zA-Z\d]+(\[\d+\])?}}/g);
  for (let i = 0; i < arr.length; i++) {
    arr[i] = arr[i].replace(/{{|}}/g, "");
    template = template.replace("{{" + arr[i] + "}}", eval("obj." + arr[i]));
  }
  return template;
}
2
3
4
5
6
7
8
直接在字符串中查找 {{*}} 并一行内替换。几个技巧:
- .*?是正则固定用法,表示非贪婪匹配,防止从第一个- {{到最后一个- }}都被匹配
- ()表示正则中的子表达式,可以用- $1-$99获取
- 如果 {}中间不包含数字(形如{1,2}),则{}本身不需要转义
- replace第二个参数支持一个回调函数,回调函数第一个参数是匹配结果,第二个参数是正则中的子表达式匹配结果(可以有 0 个或多个该参数),接下来的参数是匹配位置,最后的参数是原串本身
function render(template, obj) {
  return template.replace(/{{(.*?)}}/g, (match, key) => eval("obj." + key));
}
2
3
有没有不用 eval 的方法?
匹配时就先拿到 {{*}} 内的字母与数字。
function render(template, obj) {
  return template.replace(/{{(.*?)(\[(\d)*\])?}}/g, (match, key, index, num) => index ? obj[key][num] : obj[key]);
}
2
3
参考:
- https://wenku.baidu.com/view/15486d3f5c0e7cd184254b35eefdc8d376ee14d0.html (opens new window)
- https://corecabin.cn/2022/10/06/implement-a-template-string-replacement-and-the-data-source-is-an-object/ (opens new window)
# 6.2 字符串与正则的几个匹配方法区别
- 正则对象的方法:
- RegExp.prototype.exec():接收一个字符串参数,返回一个包含- index、- input属性的数组。- 数组的元素为匹配项和圆括号捕获的组
- 对于 g标记的正则,每次调用都会更新正则的lastIndex,即开启下一次搜索
- 搜索不到结果时,返回 null
 
- RegExp.prototype.test():接收一个字符串参数,返回是否匹配的布尔值
 
- 字符串的方法:
- String.prototype.match():完全等同于- exec,接收一个正则对象或正则字符串- 注意在 g模式下,match返回所有匹配结果的数组,这与exec不同
 
- 注意在 
- String.prototype.search():接收一个正则对象或正则字符串,返回第一个匹配位置的索引(没找到返回- -1)
- String.prototype.replace():接收两个参数,返回更新后的字符串- 第一个参数是正则对象或普通字符串(非正则)
- 第二个参数可以是一个字符串(可使用 $01-$99获取捕获字符串),也可以是一个函数,函数参数分别是匹配结果、捕获项、匹配位置、原串,返回值是替代字符串
 
 
const text = "cat, bat, sat, fat";
const pattern = /(.)at/g;
console.log(pattern.exec(text));  // ["cat", "c", index: 0, input: "cat, bat, sat, fat"]
console.log(pattern.exec(text));  // ["bat", "b", index: 5, input: "cat, bat, sat, fat"]
console.log(pattern.test(text));  // true
console.log(text.match(pattern)); // ["cat", "bat", "sat", "fat"]
console.log(text.search(pattern));  // 0
console.log(text.replace(pattern, "word($1)")); // word(c), word(b), word(s), word(f)
2
3
4
5
6
7
8
# 7 数组
# 7.1 数组的哪些原型方法会/不会改变原数组,参数、返回值是什么
- 会改变数组的方法:
- 复制和填充方法。 如 copyWithin()(从指定位置开始浅复制对应索引范围)、fill()(填充指定值到对应索引范围),返回数组本身。
- 栈方法。 如 push()(接收不定参数、返回数组最新长度)、pop()(无参、返回数组最后一项)。
- 队列方法。 如 shift()(无参、返回数组第一项)、unshift()(接收不定参数、返回数组最新长度)。
- 排序方法。 如 sort()、reverse(),返回数组本身。
- 部分操作方法。 如 splice()(传入起始索引、删除数目、添加元素,返回被删除元素数组)。
 
- 复制和填充方法。 如 
- 不会改变数组的方法:
- 迭代器方法。 如 keys()、values()、entries(),返回对应迭代器。
- 转换方法。 如 toLocaleString()、toString()、valueOf(),返回字符串或数组本身。
- 部分操作方法。 如 concat()(传入多个数组或非数组参数)、slice()(传入首尾索引),返回一个新数组。
- 搜索和位置方法。 如 indexOf()、lastIndexOf()、find()、findIndex(),返回索引值或搜索元素。
- 迭代方法。 如 every()、filter()、forEach()、map()、some(),返回布尔值、新数组或无返回值。
- 归并方法。 如 reduce()、reduceRight(),返回归并值。
 
- 迭代器方法。 如 
# 7.2 数组静态方法
- Array.of():接收不定参数,返回由不定参数返回的数组。注意- Array.of(2)和- Array(2)(或- new Array(2))的区别。- console.log(Array.of(1, 2, 3)); // [1, 2, 3] console.log(Array.of(2)); // [2] console.log(Array(2)); // [ <2 empty items> ]1
 2
 3
- Array.from():接收一个可迭代参数,返回该可迭代参数转变的数组。例如- Set、- Map、字符串或生成器对象。- const set = new Set([1, 2, 3]); const map = new Map([['a', 1], ['b', 2], ['c', 3]]); function* gen() { for (let i = 0; i < 3; i++) { yield i; } } console.log(Array.from(set)); // [1, 2, 3] console.log(Array.from(map)); // [['a', 1], ['b', 2], ['c', 3]] console.log(Array.from("hello")); // ['h', 'e', 'l', 'l', 'o'] console.log(Array.from(gen())); // [0, 1, 2]1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
- Array.isArray():判断一个对象是否为数组。
# 7.3 怎么判断数组
- Array.isArray()静态方法
- instanceof
- 调用对象 constructor属性的name
- 借用 Object.prototype.toString()方法,使其返回"[object Array]"
function isArray1(arr) {
  return Array.isArray(arr);
}
function isArray2(arr) {
  return arr instanceof Array;
}
function isArray3(arr) {
  return arr.constructor.name === 'Array';
}
function isArray4(arr) {
  return Object.prototype.toString.call(arr) === '[object Array]';
}
const arr = [1, 2, 3];
console.log(isArray1(arr)); // true
console.log(isArray2(arr)); // true
console.log(isArray3(arr)); // true
console.log(isArray4(arr)); // true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 7.4 数组去重方法
- JS 内置对象法。 无法对引用类型去重,数字和字符串相同时执行错误去重。 - function unique(arr) { const res = [], map = {}; for (let i = 0; i < arr.length; i++) { if (!map[arr[i]]) { res.push(arr[i]); map[arr[i]] = 1; } } return res; }1
 2
 3
 4
 5
 6
 7
 8
 9
 10
- 利用 - Set。- function unique(arr) { return Array.from(new Set(arr)); }1
 2
 3
- 利用 - includes()。 如果结果数组- res不存在元素- item,则加入- item。- function unique(arr) { const res = []; for (const item of arr) { if (!res.includes(item)) { res.push(item); } } return res; }1
 2
 3
 4
 5
 6
 7
 8
 9
- 利用 - indexOf()。 同上。无法对- NaN去重,因为- indexOf()基于- ===比较,而- NaN !== NaN,因此所有- NaN加入结果。- function unique(arr) { const res = []; for (const item of arr) { if (res.indexOf(item) === -1) { res.push(item); } } return res; }1
 2
 3
 4
 5
 6
 7
 8
 9
- 利用 - indexOf(),但对原数组做判断。 判断元素在原数组的下标是否等于本身。同样无法对- NaN去重,但不会有- NaN加入结果。- function unique(arr) { const res = []; for (let i = 0; i < arr.length; i++) { if (arr.indexOf(arr[i]) === i) { res.push(arr[i]); } } return res; }1
 2
 3
 4
 5
 6
 7
 8
 9
- 排序后遍历。 排序后相同的元素将被排到一起。修改了原数组,新数组未按原数组顺序,无法对 - NaN去重,且数字和字符串相同时可能无法去重。- function unique(arr) { arr.sort(); const res = [arr[0]]; for (let i = 1; i < arr.length; i++) { if (arr[i] !== arr[i - 1]) { res.push(arr[i]); } } return res; }1
 2
 3
 4
 5
 6
 7
 8
 9
 10
- 利用 - filter()和- indexOf()。 方法 5 的简写版。- function unique(arr) { return arr.filter((item, i) => arr.indexOf(item) === i); }1
 2
 3
# 7.5 手写 flat
 flat 是数组新的方法,它可以使一个数组扁平化,也就是数组嵌套数组时,将内层数组拆开,它的使用方法如下:
- flat()接收一个参数- depth,表示拍平层数,当- depth = Infinity时- flat将数组直接拍平为一维数组,当- depth省略时默认只拍平一层
- flat()会自动忽略数组中的空位
利用 reduce 实现自己的 flatten 函数:
const flatten = (arr, depth) => {
  if (depth === 0) return arr;
  return arr.reduce((acc, val) => {
    if (Array.isArray(val)) {
      return acc.concat(flatten(val, depth - 1));
    }
    return acc.concat(val);
  }, []);
}
2
3
4
5
6
7
8
9
利用扩展运算符直接拍平为一维数组:
function flatten(arr) {
  const res = [];
  for (const item of arr) {
    if (Array.isArray(item)) {
      res.push(...flatten(item));
    } else {
      res.push(item);
    }
  }
  return res;
}
2
3
4
5
6
7
8
9
10
11
# 8 对象
# 8.1 对象的原型方法与静态方法
- 对象的原型方法: - hasOwnProperty():判断对象是否存在该自有属性。
- isPrototypeOf():判断对象是否是另一对象的原型(即- obj.__proto__)。
- propertyIsEnumerable():判断对象的该属性是否是可枚举的。
- toLocaleString()、- toString()、- valueOf(),返回字符串(未重写时为- "[object Object]")或对象本身。
 
- 对象的静态方法: - Object.is():判断两个参数是否相同,一般情况下等同于- ===,但认为- NaN相等,- +0和- -0不等。- console.log(NaN === NaN); // false console.log(Object.is(NaN, NaN)); // true console.log(+0 === -0); // true console.log(Object.is(+0, -0)); // false1
 2
 3
 4
- Object.create():返回一个新对象,以第一个参数作为新对象的原型,第二个参数用于添加属性。与- new的区别是,它不会调用构造函数。- function Person () { this.age = 30; } const person = Object.create(Person.prototype, { name: { value: 'John', enumerable: true } }); console.log(person); // Person { name: 'John' }1
 2
 3
 4
 5
- Object.assign():将第二个参数开始的对象通过浅拷贝合并到第一个对象上,并返回第一个对象的引用。同名属性将按参数顺序被覆盖。- const obj1 = { a: 1, b: 2, c: 3 }; const obj2 = { d: 4, b: 5 }; const obj3 = { b: 6, e: 7 }; const obj = Object.assign(obj1, obj2, obj3) console.log(obj); // { a: 1, b: 6, c: 3, d: 4, e: 7 } console.log(obj === obj1); // true1
 2
 3
 4
 5
 6
- Object.keys()、- Object.values()、- Object.entries(),接收一个对象,返回对应数组。- 返回对象自有的、可枚举的、非 Symbol的属性
- 迭代顺序是不确定的,取决于 JS 引擎,因浏览器而异
- 注意与 Array.prototype.keys()等的区别,数组是原型方法、返回迭代器,对象是静态方法、返回数组
 
- 返回对象自有的、可枚举的、非 
- Object.fromEntries():接收一个可迭代参数,返回该可迭代参数转变的对象。可迭代参数的每个元素应为一个- entry对象,例如二维数组、包含数组的- Set、- Map、包含数组的生成器对象。- const arr = [['a', 1], ['b', 2], ['c', 3]]; const set = new Set([['a', 1], ['b', 2], ['c', 3]]); const map = new Map([['a', 1], ['b', 2], ['c', 3]]); function* gen() { for (let i = 0; i < 3; i++) { yield [i, i + 1]; } } console.log(Object.fromEntries(arr)); // { a: 1, b: 2, c: 3 } console.log(Object.fromEntries(set)); // { a: 1, b: 2, c: 3 } console.log(Object.fromEntries(map)); // { a: 1, b: 2, c: 3 } console.log(Object.fromEntries(gen())); // { '0': 1, '1': 2, '2': 3 } const obj = { a: 1, b: 2, c: 3 }; console.log(Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, v * 2]))); // { a: 2, b: 4, c: 6 }1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
- Object.getOwnPropertyNames()、- Object.getOwnPropertySymbols():返回对象自有的、无论可否枚举的属性数组- 迭代顺序是:先按升序枚举数值键,再以插入顺序(或对象字面量定义顺序)枚举字符串键和符号键 
- 如何只获取对象自有的可枚举属性?通过 - for-in加- hasOwnProperty()组合实现
- 如何只获取对象自有的不可枚举属性?通过 - Object.getOwnPropertyNames对- Object.keys()进行- filter实现,如下:- const obj = { a: 1, b: 2, c: 3 }; Object.defineProperty(obj, 'd', { value: 4, enumerable: false }); console.log(Object.getOwnPropertyNames(obj).filter(key => !Object.keys(obj).includes(key))); // ['d']1
 2
 3
 4
 5
 6
 
- Object.getOwnPropertyDescriptor()、- Object.getOwnPropertyDescriptors():返回对象某个或全体属性的描述符,包括- value、- writable、- enumerable、- configurable,如果没有该属性则返回- undefined。- const obj = { a: 1, b: 2, c: 3 }; console.log(Object.getOwnPropertyDescriptor(obj, 'a')); // { value: 1, writable: true, enumerable: true, configurable: true }1
 2
- Object.getPrototypeOf():返回对象的原型,等同于- obj.__proto__。
- Object.defineProperty()、- Object.defineProperties():定义对象属性,依次传入对象、属性名(定义单个的情况下)、描述符,返回原对象。- 也可以用来修改已有的属性
 - const obj = { a: 1, b: 2, c: 3 }; console.log(Object.defineProperty(obj, 'd', { value: 4, enumerable: false })); // { a: 1, b: 2, c: 3 } console.log(Object.defineProperties(obj, { e: { value: 5, enumerable: false }, f: { value: 6, enumerable: true } })); // { a: 1, b: 2, c: 3, f: 6 } console.log(Object.keys(obj)); // [ 'a', 'b', 'c', 'f' ] console.log(Object.getOwnPropertyNames(obj)); // [ 'a', 'b', 'c', 'd', 'e', 'f' ]1
 2
 3
 4
 5
- Object.freeze()、- Object.isFrozen():前者使一个对象被冻结并返回原对象,后者判断对象是否被冻结- 冻结后的对象无法添加、删除、修改属性,无法进行配置,无法修改对象原型
 - const obj = { a: 1, b: 2, c: 3 }; console.log(Object.freeze(obj)); // { a: 1, b: 2, c: 3 } console.log(Object.isFrozen(obj)); // true obj.a = 2; console.log(obj.a); // 11
 2
 3
 4
 5
- Object.seal()、- Object.isSealed():前者使一个对象被密封并返回原对象,后者判断对象是否被密封- 密封后的对象无法添加、删除属性,无法进行配置,但可以修改现有属性
- 一个被冻结的对象同样是被密封的
 - const obj = { a: 1, b: 2, c: 3 }; console.log(Object.seal(obj)); // { a: 1, b: 2, c: 3 } console.log(Object.isSealed(obj)); // true obj.d = 4; console.log(obj); // { a: 1, b: 2, c: 3 } console.log(Object.isFrozen(obj)); // false console.log(Object.freeze(obj)); // { a: 1, b: 2, c: 3 } console.log(Object.isFrozen(obj)); // true console.log(Object.isSealed(obj)); // true1
 2
 3
 4
 5
 6
 7
 8
 9
 10
- Object.preventExtensions()、- Object.isExtensible():前者使一个对象阻止扩展并返回原对象,后者判断对象是否可以扩展- 阻止扩展的对象无法添加属性,但可以删除和修改现有属性
- 一个被冻结或被密封的对象同样是不可扩展的
 - const obj = { a: 1, b: 2, c: 3 }; console.log(Object.isExtensible(obj)); // true console.log(Object.preventExtensions(obj)); // { a: 1, b: 2, c: 3 } console.log(Object.isExtensible(obj)); // false obj.d = 4; console.log(obj); // { a: 1, b: 2, c: 3 } delete obj.b; console.log(obj); // { a: 1, c: 3 }1
 2
 3
 4
 5
 6
 7
 8
 
# 8.2 监听对象属性变化
- 通过 - Object.defineProperty()和- setter进行监听,Vue2.0 就是用这种方法:- <ul></ul> <button onclick="update()">修改</button>1
 2- const ul = document.querySelector('ul'); const person = { name: "John", sex: "男", age: 30 }; function render() { const html = ` <li>姓名:${person.name}</li> <li>性别:${person.sex}</li> <li>年龄:${person.age}</li>`; ul.innerHTML = html; } render(); Object.keys(person).forEach(key => { let value = person[key]; Object.defineProperty(person, key, { set(val) { value = val; // 如果使用this[key] = val,会导致调用栈溢出 render(); }, get() { // get方法必须存在 return value; } }); }); function update() { person.name = "Jane"; person.sex = "女"; person.age = 20; }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- 缺点:一次性递归到底开销很大,如果数据很大,大量的递归导致调用栈溢出;不能监听对象的新增属性和删除属性;无法正确的监听数组的方法,当监听的下标对应的数据发生改变时。 
- 通过 ES6 的 - Proxy实现:- // 其他代码同上 const proxy = new Proxy(person, { // 这里不加get也可以 set(target, key, value) { target[key] = value; render(); } }); function update() { proxy.name = "Jane"; // 必须通过proxy修改 proxy.sex = "女"; proxy.age = 20; }1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
# 9 Set、Map
 # 9.1 Set、WeakSet、Map、WeakMap 间的区别
 WeakSet 和 WeakMap:
- 键值必须为对象
- 键值为弱引用,无变量引用时对象被回收
- 不可枚举、不可遍历
- 无法获取数量(无 size属性)
# 10 this 指向
 汇总: this 始终指向一个对象。
- 普通函数调用,指向全局对象
- 对象方法调用,谁调用指向谁,只看调用不看引用
- 构造函数调用,指向新构造的实例,尽管可能不被构造函数返回
- 遇 apply、call、bind,改变指向
- 匿名函数调用,一般指向全局对象
- 箭头函数调用,指向箭头函数定义域的 this
注意:在 node 下,全局 this 是一个空对象 {},注意它与全局对象 global 的区别。
# 10.1 普通函数调用
this 指向全局对象 window,其中 var 定义的全局变量视为全局对象的属性。
let user = "let";
var age = 15;
window.pwd = 123;
function fn() {
  console.log(this);	// window对象
  console.log(this.user);	// undefined
  console.log(this.age);	// 15
  console.log(this.pwd)	// 123
}
fn();
2
3
4
5
6
7
8
9
10
# 10.2 对象方法调用
谁调用,指向谁,如果函数变量被赋值给其他对象,依然是谁调用指向谁,而不考虑函数来源(只看调用,不看引用)。
var name = "me";
var age = "123";
let obj = {
  age: 15,
  fn() {
    console.log(this.name);
    console.log(this.age);
  }
}
let obj2 = {
  name: "obj2",
  age: 20
}
Object.prototype.proFn = function () {
  console.log(this.age);
}
obj.fn();	// 输出 undefined 15,obj 调用,指向 obj
obj2.fn = obj.fn;
obj2.fn();	// 输出 obj2 20,obj2 调用,指向 obj2,即使该函数是由 obj.fn 赋值
obj.proFn();	// 输出 15,obj 调用原型,指向 obj
obj2.proFn();	// 输出 20,obj2 调用原型,指向 obj2
let fn1 = obj.fn, fn2 = obj.proFn;
fn1();	// 输出 me 123,window 调用,指向 window
fn2();	// 输出 123,window 调用,指向 window
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
如果出现多层调用,则 this 指向最近的上一层对象。
let obj = {
  age: 15,
  inner: {
    age: 20,
    fn() {
      console.log(this.age);
    }
  }
}
obj.inner.fn();	// 20
2
3
4
5
6
7
8
9
10
# 10.3 构造函数调用
一般情况下,this 指向构造函数 new 的对象。如果构造函数当作普通函数使用,this 指向全局对象。
function A() {
  this.name = "me";
  var age = 15;
  console.log(this.name);	// me
  console.log(this.age);	// undefined
}
let a = new A();
console.log(a);	// A { name: 'me' }
console.log(a.name);	// me
console.log(a.age);	// undefined
2
3
4
5
6
7
8
9
10
11
特殊情况下,如果构造函数返回一个对象(包括空对象),则 new 的对象不再是构造函数的实例,而是返回的对象。
构造函数执行过程中,this 依然试图指向构造函数的实例,虽然这个实例无法返回。
function A() {
  this.name = "me";
  console.log(this.name);	// me
  console.log(this.age);	// undefined
  return {
    age: 20
  };
}
let a = new A();
console.log(a);	// { age: 20 }
console.log(a.name);	// undefined
console.log(a.age);	// 20
2
3
4
5
6
7
8
9
10
11
12
13
以上情况对返回值为基本类型、undefined、null 时失效。
function A() {
  this.name = "me";
  console.log(this.name);	// me
  console.log(this.age);	// undefined
  return null;
}
let a = new A();
console.log(a);	// A { name: 'me' }
console.log(a.name);	// me
console.log(a.age);	// undefined
2
3
4
5
6
7
8
9
10
11
# 10.4 apply、call、bind 调用
 三者均可改变 this 的指向给第一个参数。区别在于:
- apply的第二个参数为可迭代对象
- call的参数不固定
- bind除返回一个函数外,与- call并无不同,它需要调用才可以执行,其他二者直接执行函数
注意下面示例中 12、13 行的区别。
let obj = {
  age: 15,
  fn(x, y) {
    console.log(this.age + " " + x + " " + y);
  }
}
let obj2 = {
  age: 20
}
obj.fn(1, 2);	// 15 1 2
obj.fn.apply(obj2, [3, 4]);	// 20 3 4
obj.fn.call(obj2, 5, 6);	// 20 5 6
obj.fn.call(obj2, [5, 6]);	// 20 5,6 undefined
obj.fn.bind(obj2, 7, 8)();	// 20 7 8
2
3
4
5
6
7
8
9
10
11
12
13
14
# 10.5 匿名函数调用
匿名函数不属于任何对象,它的 this 一般指向全局对象。
let obj = {
  fn() {
    return function () {
      console.log(this);
    }
  },
  inner: {
    fn() {
      return function () {
        console.log(this);
      }
    }
  }
}
obj.fn()();	// window
obj.inner.fn()();	// window
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
定时器回调函数也是如此,例如:
let obj = {
  fn() {
    return function () {
      setTimeout(function () {
        console.log(this);
      }, 1000);
    }
  }
}
obj.fn()();	// window
2
3
4
5
6
7
8
9
10
但注意,事件绑定时,this 指向事件源。(此时不是严格的匿名函数)
const btn = document.getElementById("btn");
btn.onclick = function () {
  console.log(this);	// 指向 btn
}
2
3
4
const btn = document.getElementById("btn");
btn.addEventListener("click", function () {
  console.log(this);	// 指向 btn
});
2
3
4
# 10.6 箭头函数
箭头函数没有自己的 this,它会寻找定义箭头函数的作用域指向的 this。
setTimeout(() => console.log(this), 1000);	// window
let obj = {
  fn() {
    return function () {
      setTimeout(() => console.log(this), 1000);
    }
  }
}
obj.fn()();	// window
2
3
4
5
6
7
8
let obj = {
  fn() {
    return () => setTimeout(() => console.log(this), 1000);
  }
}
obj.fn()();	// obj
2
3
4
5
6
箭头函数作为对象属性值时,是对象定义域的 this。
let obj = { fn: () => console.log(this) };
obj.fn(); // window
let obj2 = {
  fn() {
    return { fn: () => console.log(this) };
  }
};
obj2.fn().fn(); // obj2,{ fn: [Function: fn] }
2
3
4
5
6
7
8
箭头函数不可被 apply、call、bind 改变 this 的指向。
let obj = { fn: () => console.log(this) };
let obj2 = { a: 123 };
obj.fn.call(obj2);  // window
2
3
再看这个示例:
let obj = {
  name: "me"
}
function fn() {
  console.log(this);
  return () => console.log(this);
}
fn()();	// 指向全局对象,输出两次 window
let fn2 = fn.call(obj);	// 指向 obj,输出 obj 即 { name: 'me' }
fn2();	// 返回的箭头函数中的 this 指向上层的 this 即 obj,输出 obj 即 { name: 'me' }
2
3
4
5
6
7
8
9
10
# 11 apply、call、bind
 apply、call 和 bind 都可以改变函数内 this 的指向,不同之处在于:
- apply的第一个参数是- this指向的对象,第二个参数是一个可迭代对象,如- Math.max.apply(null, [1, 2, 3, 4, 5])
- call的第一个参数是- this指向的对象,剩余参数数量不固定,如- Math.max.apply(null, 1, 2, 3, 4, 5)
- bind与- call一致,但他返回一个改写了- this指向的函数,并不立即执行
# 11.1 手写 bind
 Function.prototype.bind = function (context, ...args) {
  var fn = this;
  var noop = function () {};
  var res = function (...rest) {
    return fn.apply(this instanceof noop ? this : context, [...args, ...rest]);
  }
  if (this.prototype) {
    noop.prototype = this.prototype;
  }
  res.prototype = new noop();
  return res;
}
2
3
4
5
6
7
8
9
10
11
12
解释:
- context是要绑定的对象,- ...args是- bind传入的不定参数。
- res是返回的函数,如果只需要更改原函数- fn中- this的指向,这么写就可以了:- Function.prototype.myBind = function (context, ...args) { var fn = this; // 原函数 var res = function (...rest) { return fn.apply(context, [...args, ...rest]); }; return res; }1
 2
 3
 4
 5
 6
 7
- 注意到原函数 - fn可能被当做构造函数使用,此时- fn中的- this应指向构造实例,与- bind绑定的对象无关。为了保证- res的实例也是一个- fn实例,我们有两种方法:- 让 res.prototype指向this.prototype,但这种方法会导致修改res的原型方法时导致fn的原型方法也被修改
- 让 res通过原型链继承this(即res.prototype = new this()),但这种方法执行了fn函数,会带来不必要的后果
 - Function.prototype.myBind = function (context, ...args) { var fn = this; // 原函数 var res = function (...rest) { // 这里的 this 与外层的 this 不同,如果 res 是普通函数,this 指向 window,如果 res 是构造函数,this 指向实例 return fn.apply(this instanceof res ? this : context, [...args, ...rest]); }; if (this.prototype) { // res.prototype = this.prototype; // res.prototype = new this(); } return res; }1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
- 让 
- 因此,我们可以构造一个 - noop函数,它是- fn构造函数的复制品,但是不做任何执行,让它绑定到- fn并让- res继承它:- Function.prototype.myBind = function (context, ...args) { var fn = this; // 原函数 var noop = function () {}; // 空函数,仿造原构造函数,不破坏原型链且不做执行 var res = function (...rest) { // 这里的 this 与外层的 this 不同,如果 res 是普通函数,this 指向 window,如果 res 是构造函数,this 指向实例 return fn.apply(this instanceof res ? this : context, [...args, ...rest]); }; if (this.prototype) { noop.prototype = this.prototype; // 获得原函数的原型 } res.prototype = new noop(); // 继承原函数 return res; }1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
# 12 原型
- prototype:每个函数都有如此属性,其关联到另一个对象(称为原型对象)
- __proto__:每个对象都有如此属性,指向其构造器函数的原型对象(父对象);函数也是对象,该属性指向- Function的原型对象(- Function.prototype)- 除了 a.__proto__获取,还可以通过Object.getPrototypeOf(a)获取
- a.__proto__ === A.prototype
 
- 除了 
- constructor:每个原型对象都有如此属性,指向构造器函数- 如果实例调用 constructor属性,由于不存在的属性会从原型链寻找,因此也指向其构造函数
- A.prototype.constructor === A
 
- 如果实例调用 

细节问题:
- a.__proto__ === A.prototype说明什么?- a是构造函数- A的一个直接实例,或者说- a是通过- new A()构造的。
- a instanceof X说明什么?- a是一个- X;- a是构造函数- X的一个直接或间接实例;构造函数- X的- prototype出现在- a的原型链上。
- a的原型链是什么?- a.__proto__、- a.__proto__.__proto__、……一直到- null就是- a的原型链。
- A的原型链是什么? 函数- A无论从何处继承,它的原型链只有:- A -> Function.prototype -> Object.prototype -> null。
- 调用 - a.fun(),- fun定义在何处? 可能在:- a本身、构造函数- A的- prototype上、原型链上的某个- X.prototype上。
- 什么对象没有原型?什么函数没有原型对象? - 通过 Object.create(null)创建的空对象没有原型,而{}是有的(指向Object.prototype)
- Object.prototype的原型指向- null
- 箭头函数无法作为构造函数使用,因此没有原型对象
 - console.log(Object.create(null).__proto__); // undefined console.log(Object.prototype.__proto__); // null console.log((() => {}).prototype); // undefined1
 2
 3
- 通过 
# 12.1 new 一个对象的过程
 以 var a = new A() 为例:
- 创建一个空对象:var obj = {}
- 让构造器函数 A的this指向对象obj,并执行A中的函数体:var result = A.call(obj)
- 设置原型链,让对象 obj的__proto__属性指向构造器函数A的原型对象:obj.__proto__ = A.prototype
- 判断构造器函数 A的返回类型,如果是值类型(包括null),结果返回obj,如果是引用类型,返回引用类型的对象:a = result && typeof result === "object" ? result : obj
# 12.2 手写 new 函数
 Function.prototype.new = function () {
  var obj = {};
  var result = this.call(obj);
  obj.__proto__ = this.prototype;
  return result && typeof result === "object" ? result : obj;
}
2
3
4
5
6
# 12.3 原型与 in 操作符、Object 方法
 - for-in:遍历对象可枚举的非- Symbol属性,会遍历自有属性和原型属性
- in:判断对象是否存在该属性,包括自有属性和原型属性
- Object.prototype.hasOwnProperty():判断对象是否存在该自有属性
- Object.keys():返回对象自有的、可枚举的、非- Symbol的属性名称数组
- Object.getOwnPropertyNames():返回对象自有的、无论可否枚举的、非- Symbol的属性名称数组
- Object.getOwnPropertySymbols():返回对象自有的、无论可否枚举的、- Symbol属性数组
function Person() {}
Person.prototype.name = "Carlo";
Person.prototype.age = 24;
const person = new Person();
person.job = "Developer";
Object.defineProperty(person, "country", { value: "China", enumerable: false });
const s = Symbol("s");
person[s] = "symbol";
for (const key in person) {
  console.log(key); // job name age
}
console.log("job" in person); // true
console.log("name" in person); // true
console.log("country" in person); // true
console.log(s in person); // true
console.log(person.hasOwnProperty("job")); // true
console.log(person.hasOwnProperty("name")); // false
console.log(person.hasOwnProperty("country")); // true
console.log(person.hasOwnProperty(s)); // true
console.log(Object.keys(person)); // ["job"]
console.log(Object.getOwnPropertyNames(person)); // ["job", "country"]
console.log(Object.getOwnPropertySymbols(person)); // [Symbol(s)]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
对象属性枚举顺序:
- for-in、- Object.keys():不确定,取决于JS引擎,因浏览器而异
- Object.getOwnPropertyNames()、- Object.getOwnPropertySymbols()、- Object.assign():先按升序枚举数值键,再以插入顺序(或对象字面量定义顺序)枚举字符串键和符号键
# 12.4 原型赋值问题
以下代码输出什么?
function Test() {}
Test.prototype.n = 0;
Test.prototype.inc = function() {
  this.n += 1;
  return this.n;
}
t1 = new Test();
t2 = new Test();
console.log(t1.inc());  // 1
console.log(t2.inc());  // 1
2
3
4
5
6
7
8
9
10
11
因为第 4 行的 this.n += 1 也就是 this.n = this.n + 1。右边的 this.n 由于实例没有 n 属性会从原型中获取,赋值到左边的 this.n,会给实例新增 n 属性,这时返回的都是实例的 n 属性。
以下代码输出什么?
function Test() {}
const test = new Test();
Test.prototype = {
  name: "test"
};
console.log(test.name); // undefined
2
3
4
5
6
在 new 一个对象时,观察 11.1 提到的过程,可以发现实例的 __proto__ 属性指向了构造函数的原型对象 Test.prototype,称之为 A。当让 Test.prototype 指向一个新对象(称之为 B)时,test.__proto__ 仍保持对 A 的指向,因此输出 undefined。
注意与以下代码的区别:
function Test() {}
const test = new Test();
Test.prototype.name = "test";
console.log(test.name); // test
2
3
4
# 13 闭包
闭包(closure):内部函数可以访问外部函数的局部变量,即使外部函数被销毁。本质是 函数内部嵌套函数。
function fun1() {
  var value = 0;
  return function () {
    value++;
    console.log(value);
  }
}
var fun = fun1();
fun();	// 1
2
3
4
5
6
7
8
9
闭包的作用:隐藏变量(通过访问局部变量的形式,间接访问变量),避免全局变量污染,设置私有变量等。
闭包的原理:作用域链,即当前作用域可以访问上级变量。注意,这里指的是函数声明时的作用域链,而非调用时的作用域链,参考闭包作为参数传递的情况。
参考:
- https://blog.csdn.net/Matildan/article/details/108349502 (opens new window)
- https://blog.csdn.net/dovlie/article/details/76339244 (opens new window)
- https://zhuanlan.zhihu.com/p/22486908 (opens new window)
# 13.1 闭包的应用
- 函数作为返回值:返回的函数调用时可以访问外部函数的局部变量,示例见上 
- 闭包作为参数传递:函数作为参数传递时,访问的变量是该函数作用域下的变量,而非调用函数作用域下的变量 - var num = 15; var fun1 = function (x) { console.log(x > num); } var fun2 = function (fun) { var num = 100; fun(30); } fun2(fun1); // true 访问的是 fun1 作用域的变量 num,即全局变量 num(15)1
 2
 3
 4
 5
 6
 7
 8
 9
- 定时器与闭包:分析以下几段代码的区别 - for (var i = 0; i < 3; i++) { setTimeout(function () { console.log(i); }, 1000); } // 1 s 后,直接输出 3 3 3,因为先执行 for,再执行任务队列的 setTimeout for (var i = 0; i < 3; i++) { (function (i) { setTimeout(function () { console.log(i); }, 1000); }(i)); } // 1 s 后,直接输出 0 1 2,因为任务队列的 setTimeout 传递了参数 i for (var i = 0; i < 3; i++) { (function (i) { setTimeout(function () { console.log(i); }, 1000 * i); }(i)); } // 每隔 1 s 输出 0 1 2,因为同时启动了三个不同时间的定时器 for (let i = 0; i < 3; i++) { setTimeout(function () { console.log(i); }, 1000); } // 1 s 后,直接输出 0 1 2,因为 let 每次循环中创建的是同名变量,不影响其他定时器访问 i1
 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
- 对对象实现封装:将对象的属性改为函数的局部变量 - const obj = (function () { var value = 0; return { add: function () { value++; }, getValue() { return value; } } })(); obj.add(); console.log(obj.getValue()); // 11
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
# 13.2 闭包会造成内存泄漏吗?
大部分人认为,闭包的局部变量常驻内存,会增大内存使用量,并且使用不当很容易造成内存泄露。
内存泄漏
用不到(访问不到)的变量,依然占据内存空间,无法被再次利用。
但这取决于 JS 引擎。对于 Chrome 的 V8 引擎,它可以回收闭包中不使用的局部变量,保留使用的局部变量的引用。其他的浏览器引擎也有各自的处理方式。对于 IE8 的 JScript.dll 引擎,它会完整保留闭包的所有变量绑定,造成一定的内存消耗。
所以,我觉得说闭包一定能造成内存泄漏是不对的。对于闭包使用到的变量,我们需要使用它并占据空间;对于闭包不使用的变量,是否占据空间取决于 JS 引擎。
参考:https://www.cnblogs.com/rubylouvre/p/3345294.html (opens new window)
# 14 迭代器、生成器
迭代器是一个对象,通过不停地调用 next() 方法可以迭代得到多个值。
生成器是一个函数,通过 yield 关键字操作,生成一个生成器对象,可作为迭代器使用。
# 14.1 迭代器
迭代器是一个对象,实现了 Iterable 接口(next()、return() 等方法)。
- 可迭代对象内置默认迭代器函数,并位于 - Symbol.iterator作为键的属性上,该函数执行后生成一个迭代器。- 字符串、数组、映射、集合、 - arguments对象、- NodeList等 DOM 集合类型都内置该工厂函数,也就是说它们是可迭代对象。
- 得到一个可迭代对象的迭代器: - const str = "abc"; const f = str[Symbol.iterator]; console.log(f); // [Function: [Symbol.iterator]] console.log(f.call(str)); // Object [String Iterator] {}1
 2
 3
 4
 
- 迭代器可以进行以下操作: - for-of循环、数组解构、扩展运算符、- Array.from()、- Set、- Map()、- Promise.all()、- Promise.race()、- yield*。- 如果上述操作的目标是可迭代对象,则使用它们的迭代器,例如 for(const ch of str)和for (const ch of str[Symbol.iterator]())是一致的。
 
- 如果上述操作的目标是可迭代对象,则使用它们的迭代器,例如 
- 迭代器的 - next()方法返回一个对象,这个对象有- value和- done两个属性,当- done属性为- false时,上述操作暂停迭代。
- 一个普通对象可以通过添加 - Symbol.iterator属性,属性值设置为一个函数,函数返回实现- Iterator接口的对象,这个对象就可以进行上述操作。- const obj = { a: 1, b: 2, c: 3, [Symbol.iterator]() { let i = 0; const that = this; const keys = Object.keys(this); return { // 通过闭包来保存状态 next() { if (i < keys.length) { return { value: that[keys[i++]], done: false }; } else { return { value: undefined, done: true }; } } }; } }; for (let value of obj) { console.log(value); // 1 2 3 }1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
- 迭代器对象有一个可选的 - return()方法,迭代终止时会调用它,并“关闭”迭代器。如果迭代器没有关闭,可以从上次离开的地方继续迭代。但主动调用它不会使迭代器关闭,因此给不可关闭的迭代器(如数组迭代器)添加- return()方法不能使数组迭代器变成可关闭的。- const arr = [1, 2, 3, 4, 5]; let it = arr[Symbol.iterator](); it.return = function() { console.log('return here'); return { value: undefined, done: true }; } let [ a, b, c ] = it; // return here console.log(a, b, c); // 1 2 3 let [ d, e ] = it; // return here console.log(d, e); // 4 51
 2
 3
 4
 5
 6
 7
 8
 9
 10
# 14.2 生成器
生成器是一个函数。
- function* () {}是一个生成器函数,它的返回值是一个处于暂停状态的生成器对象,类似于迭代器,生成器对象实现了- Iterable接口并自引用。
- yield关键字暂停生成器的执行,并将- yield表达式的值包装成- { value, done }的形式传递给生成器对象;生成器对象通过- next()恢复生成器函数的执行并取得下一个- yield值。
- 如果生成器函数有 - return,则- return作为最后一个迭代值。
- yield表达式的返回值是生成器对象调用- next()传入的值(第一个- next()的参数会被忽略,因为要执行函数)。- function* generator(val) { console.log(val); const newVal = yield 1; console.log(newVal); yield newVal; return 3; } const gen = generator("a"); // a,val 的值 console.log(gen.next("b")); // { value: 1, done: false },参数 "b" 被忽略 console.log(gen.next("c")); // { value: 'c', done: false },参数 "c" 被 yield 1 返回并赋值给 newVal console.log(gen.next("d")); // { value: 2, done: true },返回值 2 console.log(gen.next("e")); // { value: undefined, done: true },迭代已结束1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
- yield*表达式可以将后面的一个可迭代对象依次进行- yield。- function* generator() { yield [1, 2, 3]; yield* [4, 5, 6]; } const gen = generator(); console.log(gen.next()); // { value: [1, 2, 3], done: false } console.log(gen.next()); // { value: 4, done: false } console.log(gen.next()); // { value: 5, done: false } console.log(gen.next()); // { value: 6, done: false } console.log(gen.next()); // { value: undefined, done: true }1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
- yield*表达式后可以跟生成器本身,因此可以实现递归:- function* nTimes(n) { if (n > 0) { yield* nTimes(n - 1); yield n; } } for (let x of nTimes(3)) { console.log(x); // 1 2 3 }1
 2
 3
 4
 5
 6
 7
 8
 9
 10
- 生成器对象调用 - return()方法会使生成器强行关闭,并使用- return()的参数作为最终的- value,- done为- true;生成器关闭后不可恢复。
- 生成器对象调用 - throw()方法会使生成器暂停并抛出错误,如果错误未被处理,则生成器被关闭;如果生成器函数内部处理了错误,则生成器不会关闭且可恢复执行,但会跳过一次- yield。
# 15 Promise 与异步
 Promise 是异步编程的一种解决方案,解决地狱回调的问题。
- Promise的作用:- Promise是异步微任务,解决了异步多层嵌套回调的问题,让代码的可读性更高,更容易维护。
- Promise的使用:- Promise是 ES6 提供的一个构造函数,可以使用- Promise构造函数- new一个实例,- Promise构造函数接收一个函数作为参数,这个函数有两个参数,分别是两个函数- resolve和- reject,- resolve将- Promise的状态由等待变为成功,将异步操作的结果作为参数传递过去;- reject则将状态由等待转变为失败,在异步操作失败时调用,将异步操作报出的错误作为参数传递过去。实例创建完成后,可以使用- then方法分别指定成功或失败的回调函数,也可以使用- catch捕获失败,- then和- catch最终返回的也是一个- Promise,所以可以链式调用。
- Promise的特点:- 对象的状态不受外界影响(Promise对象代表一个异步操作)。有三种状态:pending(执行中)、resolved(成功,又称fulfilled)、rejected(拒绝) ,其中pending为初始状态,fulfilled和rejected为结束状态(结束状态表示Promise的生命周期已结束)。
- 一旦状态改变,就不会再变,任何时候都可以得到这个结果。 Promise对象的状态改变,只有两种可能(状态凝固了,就不会再变了,会一直保持这个结果):从pending变为resolved、从pending变为rejected。
- resolve方法的参数是- then中回调函数的参数,- reject方法中的参数是- catch中的参数。
- then方法和- catch方法只要不报错,返回的都是一个- fullfilled状态的- Promise,解决值为回调函数的返回值。
 
- 对象的状态不受外界影响(
- Promise的静态方法:- Promise.resolve():返回的- Promise对象状态为- fulfilled,并且将该- value传递给对应的- then方法。我们认为- Promise.resolve()是普通- Promise对象调用- resolve()的语法糖,即:- Promise.resolve(3); // 等同于 new Promise((resolve, reject) => resolve(3));1
 2
 3- 但如果传入的是一个 - Promise对象,那么- Promise.resolve()将不做任何修改、原封不动地返回这个实例。
- Promise.reject():返回一个状态为- rejected的- Promise对象,并将给定的失败信息传递给对应的处理方法。
- Promise.all():返回一个新的- Promise对象,该- Promise对象在参数对象里所有的- Promise对象都成功的时候才会触发成功,解决值为所有解决值的数组;一旦有任何一个- iterable里面的- Promise对象失败则立即触发该- Promise对象的失败,失败值为该失败值。- const one = Promise.resolve("one"); const two = Promise.resolve("two"); const three = Promise.reject("three"); Promise.all([one, two]).then(console.log); // [ 'one', 'two' ] Promise.all([one, three]).catch(console.log); // three1
 2
 3
 4
 5
- Promise.race():当参数里的任意一个子- Promise被成功或失败后,父- Promise马上也会用子- Promise的成功返回值或失败详情作为参数调用父- Promise绑定的相应句柄,并返回该- Promise对象。注意子- Promise的落定顺序可能被- setTimeout改变。- const one = new Promise((resolve, reject) => { setTimeout(resolve, 500, 'one'); }); const two = new Promise((resolve, reject) => { setTimeout(resolve, 100, 'two'); }); Promise.race([one, two]).then(console.log); // two1
 2
 3
 4
 5
 6
 7
 
# 15.1 前端怎么实现异步
JS 是单线程,程序运行是同步的。但如果有的代码长时间执行会影响后面代码的运行,因此需要异步编程。
基本原理是将异步代码加入任务队列,当当前调用栈清空后再取出任务队列中的任务。
常用方法:
- 回调函数。
- 事件监听。
- 发布/订阅。
- Promise。
- 生成器。
- async/await异步函数。
# 15.2 宏任务和微任务
宏任务(macro task)可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。
微任务(micro task)可以理解是在当前宏任务执行结束后立即执行的任务。
- 宏任务:整体代码、new Promise内的代码、setTimeout、setInterval、I/O事件、UI交互事件等
- 微任务:Promise.then、process.nextTick()
如果微任务内有新的宏任务,新的宏任务会等当前微任务队列清空后再执行。
# 16 class 与继承
 class 是 ES6 新增关键字,用于定义一个类,可通过 constructor() 初始化,可被 extends 关键字继承;实际上是构造函数的语法糖,基于组合寄生式继承。
# 16.1 JS 继承的几种方式
继承方式:原型链继承、借用构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承、ES6 Class。
参考:
- https://blog.csdn.net/jatej/article/details/120317973 (opens new window)
- https://blog.csdn.net/m0_45857808/article/details/123866779 (opens new window)
- 原型链继承:将子类的原型对象设置为父类的一个实例。 - 原理:子类的实例可以使用原型链的所有属性和方法,包括父类构造函数的和父类原型的。
- 缺点:新实例无法向父类构造函数传参;继承单一;所有新实例都会共享父类实例的属性。
 - function Parent() { this.name = 'parent'; this.nums = [1, 2, 3]; } function Child() { this.type = 'child'; } Child.prototype = new Parent(); // 原型链继承 const c1 = new Child(); console.log(c1.name); // parent,来自原型,即父类实例 const c2 = new Child(); c1.nums.push(4); console.log(c2.nums); // [1, 2, 3, 4],nums来自原型,所以c1和c2共享nums1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
- 借用构造函数继承:调用父类构造函数,但通过 - call把内部的- this指向子类实例。- 原理:父类构造函数的 this指向子类实例,因此子类实例获得了父类属性和方法。子类可继承多个父类(call多个),同时子类可向父类传参。
- 缺点:不能继承父类原型属性;无法实现构造函数复用;每个新实例都有父类构造函数的副本。
 - function Parent(name) { console.log('Parent constructor'); this.name = name; } Parent.prototype.say = function() { console.log('Parent say'); } function Child(name) { Parent.call(this, name); // 构造函数继承 this.type = 'child'; } const c1 = new Child("child1"); // Parent constructor console.log(c1.name); // child1 console.log(c1.say); // undefined const c2 = new Child("child2"); // Parent constructor,每次都要调用父类构造函数1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
- 原理:父类构造函数的 
- 组合继承:组合使用原型链继承和构造函数继承。 - 原理:同时使用这两种继承方式,父类实例的属性和方法被直接继承到子类实例,父类原型的属性和方法通过原型链获取,这样不会引起共享属性问题。同时子类可以传参。
- 缺点:调用了两次父类构造函数。
 - function Parent(name) { console.log('Parent constructor'); this.name = name; } Parent.prototype.say = function() { console.log('Parent say'); } function Child(name) { Parent.call(this, name); this.type = 'child'; } Child.prototype = new Parent(); // Parent constructor const c = new Child("child"); // Parent constructor console.log(c.name); // child c.say(); // Parent say1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
- 原型式继承:使用一个函数,传入父类实例,函数体内定义子类,并进行原型链继承,最后返回子类实例。等同于 - Object.create()方法。- 原理:与原型链继承方法类似,只是进行了一次封装,没有显式定义子类构造函数。
- 缺点:与原型链继承方法类似,所有新实例都会共享父类实例的属性;无法实现复用。
 - function Parent(name) { this.name = name; } const parent = new Parent('parent'); function createObj(parent) { function Child() {} Child.prototype = parent; return new Child(); } const child = createObj(parent); // 等同于 Object.create(parent) console.log(child.name); // parent1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
- 寄生式继承:基于原型式继承,但外加一层函数封装,为子类实例添加属性或方法。 - 原理:与原型式继承类似。
- 缺点:与原型式继承类似。
 - function Parent(name) { this.name = name; } const parent = new Parent('parent'); function clone(obj) { const child = Object.create(obj); child.getName = function() { return this.name; } return child; } const child = clone(parent); console.log(child.name); // parent console.log(child.getName()); // parent1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
- 组合寄生式继承:结合组合式继承和寄生式继承,使用一个空构造函数仿造父类构造函数。是相对最优的继承方式。 - 原理:组合式继承的构造函数继承仍不变,然后使用一个空函数,将其原型对象赋值为父类的原型对象以实现仿造,再让子类通过原型链继承继承空函数(执行空函数无副作用),然后修改子类原型对象的 constructor指向。这样子类继承了父类的所有私有属性、方法并可使用父类原型的属性、方法。
- 缺点:无。
 - function Parent(name) { this.name = name; this.nums = [1, 2, 3]; } Parent.prototype.say = function() { console.log("Parent say"); } function Child(name, age) { Parent.call(this, name); this.age = age; } const F = function() {}; F.prototype = Parent.prototype; Child.prototype = new F(); Child.prototype.constructor = Child; const child = new Child("child", 10); console.log(child); // { name: 'child', nums: [ 1, 2, 3 ], age: 10 } console.log(child.name); // child child.say(); // Parent say child.nums.push(4); // child.nums 为 [ 1, 2, 3, 4 ]1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23- 由于第 14-16 行可以抽象为原型式继承,因此可以将组合寄生式继承(即第 14-17 行)整理为函数: - function extend(Child, Parent) { Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child; }1
 2
 3
 4
- 原理:组合式继承的构造函数继承仍不变,然后使用一个空函数,将其原型对象赋值为父类的原型对象以实现仿造,再让子类通过原型链继承继承空函数(执行空函数无副作用),然后修改子类原型对象的 
# 17 内存回收
浏览器的 JavaScript 具有自动垃圾回收机制(GC: Garbage Collection),也就是说,执行环境会负责管理代码执行过程中使用的内存。
其原理是:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。但是这个过程不是实时的,因为其开销比较大并且 GC 时停止响应其他操作,所以垃圾回收器会按照固定的时间间隔周期性的执行。
# 17.1 内存回收的两个方法
- 引用计数(reference counting):将对象被其他对象引用的情况计数,如果没有其他对象指向该对象(零引用),对象被 GC 回收。但无法处理循环引用。
- 标记清除(mask sweep):判断对象是否可以获得,从根开始找所有根引用的对象,然后找对象引用的对象,以此类推,直到最终无法找到的对象被清除。
IE6,7 使用引用计数方式对 DOM 对象进行垃圾回收,从 2012 年起,所有现代浏览器都使用了标记清除垃圾回收算法。
不可获得的对象 = 有零引用的对象(1) + 循环引用的无用对象(2) + 无法从根对象查询到的对象(3)
很显然,标记清除算法可以对(2)进行很好地清理,但是会误清理掉(3),但是实践中我们很少会碰到类似的情况,所以开发者不太会去关心垃圾回收机制。
WeakMap 里面对键值的引用就是弱引用,不会被计入垃圾回收机制,如果键值对象被回收,那么 WeakMap 对应的键值对会自动消失。
# 17.2 内存泄漏的情况
- 意外的全局变量
- 闭包(参考闭包一节)
- 未被清空的定时器
- 未被销毁的事件监听
- DOM 引用
# 18 防抖和节流
防抖和节流用来处理 响应跟不上触发频率 的情况,如鼠标移动事件、滚动条滚动事件等。
两者的区别: 防抖的周期随函数触发而后移,节流的周期不随函数触发而后移。防抖适合处理高频但停顿的事件,节流适合处理高频且连续的事件。
参考:
- https://www.cnblogs.com/wjgoblin/p/10950886.html (opens new window)
- https://blog.csdn.net/hupian1989/article/details/80920324 (opens new window)
# 18.1 防抖
防抖: 将几次操作合并为一次操作进行,只有最后一次操作 被触发。
示例:用户 resize 调整窗口大小,会发生多次 resize 事件,利用防抖使用户最终停下来后再触发事件;用户在搜索框打字,当用户停下来后触发关键词联想事件。
实现过程:防抖分延迟防抖(先周期后触发)和前缘防抖(先触发后周期),下面以延迟防抖为例。设定定时器,延时触发函数,但若延迟时间内函数再次被触发,取消之前的计时器重新计时,如此,只有最后一次操作被触发。
function debounce(fn, delay) {
  let timer = null;
  return function () {
    const that = this, args = arguments;
    timer && (clearTimeout(timer), timer = null);
    timer = setTimeout(() => fn.apply(that, args), delay);
  }
}
2
3
4
5
6
7
8
- 这里的 - fn.apply起何作用?- 为了把调用节流函数返回的匿名函数的 - this和- arguments传给- fn使用,绑定事件后,这里- this为全局对象- window(通常不用),- arguments包含事件。
- 可以有别的写法吗? - 可以使用 ES6 的扩展运算符,写成 - setTimeout(() => fn(...args), delay),本质上,就是调用- fn函数并传不确定的参数。
# 18.2 节流
节流: 一段时间内只触发一次函数,若周期内有新触发,不执行,除非周期结束且开始触发新周期。
示例:用户 resize 调整窗口大小,会发生多次 resize 事件,利用节流使用户在调整过程中以稳定的节奏触发事件。
实现过程:节流分延迟节流(先周期后触发)和前缘节流(先触发后周期),下面以延迟节流为例。设定定时器,延时触发函数,但若延迟时间内函数再次被触发,不予操作且不予重新计时,直到函数执行后再清除计时器。
function throttling(fn, delay) {
  let timer = null;
  return function () {
    const that = this, args = arguments;
    !timer && (timer = setTimeout(() => {
      fn.apply(that, args);
      clearTimeout(timer);
      timer = null;
    }, delay));
  }
}
2
3
4
5
6
7
8
9
10
11
比较两个代码,就很好理解,一个是停顿处理,一个是平滑处理。
# 19 深拷贝和浅拷贝
- 浅拷贝(赋值): 与原数据指向相同,无论如何改变新数据,原数据都将被改变
- 深拷贝: 与原数据指向不同,无论如何改变新数据,都不会使原数据改变
# 19.1 JSON转换
JSON.stringify 将 JavaScript 对象转换为字符串,JSON.parse 将 JavaScript 字符串转换为对象,据此进行深拷贝。
function deepCopy(source) {
  if (!isObject(source)) {
    return source;
  }
  return JSON.parse(JSON.stringify(source));
}
function isObject(obj) {
  return obj && typeof obj === "object";
}
2
3
4
5
6
7
8
9
10
缺点:
- 会破坏部分数据:
- 时间对象会被复制为字符串,而非时间对象
- RegExp对象、- Error对象会被复制为空对象- {}
- NaN、- Infinity、- -Infinity会被复制为- null
- 函数和 undefined会直接丢失
 
- 会舍弃原对象的原型链,直接继承自 Object.prototype或Array.prototype
- 不能处理循环引用的情况
- 当数据层次很深时,会栈溢出
# 19.2 普通递归
讨论原数据的类型,可能为:值类型(包括 null)、对象、数组,然后递归处理子层数据。
function deepCopy(source) {
  if (!isObject(source)) {
    return source;
  }
  const target = Array.isArray(source) ? [] : {};
  for (let key in source) {
    if (source.hasOwnProperty(key)) {
      target[key] = deepCopy(source[key]);
    }
  }
  return target;
}
function isObject(obj) {
  return obj && typeof obj === "object";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
缺点:
- 同样会舍弃原对象的原型链,两个解决方案:
- 返回结果前加上 target.__proto__ = source.__proto__;
- 第 5 行改为 const target = new source.constructor;
 
- 返回结果前加上 
- 同样不能处理循环引用的情况
- 当数据层次很深时,会栈溢出
# 19.3 循环引用优化
用 Map 记录对象中的所有对象,并记录对象的引用关系,即每个出现过的对象,有一个之前创建过的新对象与之对应,拿出新对象返回。
const map = new Map();
function deepCopy(source) {
  if (!isObject(source)) {
    return source;
  }
  if (map.has(source)) {
    return map.get(source);
  } else {
    const target = new source.constructor;
    map.set(source, target);
    for (let key in source) {
      if (source.hasOwnProperty(key)) {
        target[key] = deepCopy(source[key]);
      }
    }
    return target;
  }
}
function isObject(obj) {
  return obj && typeof obj === "object";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
缺点:
- 当数据层次很深时,会栈溢出
- 对正则等特殊对象不能较好地处理
# 19.4 jQuery.extend() 函数
 第一个参数表示是否深拷贝,第二个参数为目标对象,第三个参数为原对象。
function deepCopy(source) {
  if (!isObject(source)) {
    return source;
  }
  const target = new source.constructor;
  $.extend(true, target, source);
  return target;
}
function isObject(obj) {
  return obj && typeof obj === "object";
}
2
3
4
5
6
7
8
9
10
11
12
# 20 跨域请求与同源策略
不同源的 ajax 请求,会出现跨域问题。
同源策略:同源指 协议、域名、端口 相同,子域名也不同源。一段脚本只能读取同源信息。假如没有同源策略,黑客可以在他页面利用 iframe 嵌入某个登录界面,并用 JS 读取 input 内容、cookies 等。
同源策略会导致这些限制:cookie、LocalStorage、IndexDB无法读取;DOM 无法获得、ajax 请求不能发送。
跨域发生的时机:请求发送到后端,后端返回数据,浏览器接收数据时被跨域报错拦截。而非发出请求时。
# 20.1 解决跨域的方法
有 jsonp 跨域、document.domain、服务器代理、window.name、window.postMessage、WebSocket、CORS 等方法。
- jsonp 跨域。jsonp 只能解决 - GET请求。- script的- src、- link的- href、- img的- src不受跨域限制。通过- <script>的- src属性,服务器动态生成JS脚本,通过调用本地回调函数返回数据。- <script> function fn(rs) { console.log(rs); } </script> <html> <script src="http://localhost:3000/get"></script> </html>1
 2
 3
 4
 5
 6
 7
 8- const express = require('express'); const app = express(); app.get('/get', (req, res) => { const data = JSON.stringify({a: 1, b: 2}); // 传递数据 const fnStr = `fn(${data})`; // 函数调用语句 res.end(fnStr); }); app.listen(3000, () => { console.log('通过 3000 端口访问'); });1
 2
 3
 4
 5
 6
 7
 8
 9
 10- 也可以用 jQuery 直接把 ajax 封装为 jsonp: - $.ajax({ type: 'GET', url: 'http://localhost:3000/get', success: function (result) { console.log(result); }, dataType: 'jsonp' // 必须指定 });1
 2
 3
 4
 5
 6
 7
 8- const express = require('express'); const app = express(); app.get('/get', (req, res) => { const data = JSON.stringify({a: 1, b: 2}); // 传递数据 // res.json(data); res.jsonp(data); }); app.listen(3000, () => { console.log('通过 3000 端口访问'); });1
 2
 3
 4
 5
 6
 7
 8
 9
 10
- document.domain。通过给- document.domain赋值可以设置当前域为 当前域的父域,即可访问 cookie 和 DOM。- console.log(document.domain); // a.test.com document.domain = 'test.com';1
 2
- 服务器代理。内部服务器代理请求跨域 URL,然后返回数据。静态浏览器和代理服务器同源,代理服务器再向后端服务器发请求,服务器之间不存在同源限制。如 Nginx、node 接口等。 
- window.name。在同一个窗口里,- window.name可以在前一个网页设置,再后一个网页获取,即- window.name = data。
- window.postMessage。该方法允许跨窗口通信,例如父窗口向子窗口发消息。- var popup = window.open('http://bbb.com', 'title'); // 打开子窗口 popup.postMessage('Hello World!', 'http://bbb.com'); // 信息内容,接收消息的源(发向地)1
 2- 该方法还可以实现读写其他窗口的 - localStorage。
- ** - WebSocket。**如果- WebSocket的头信息的- Origin在白名单内,服务器就允许跨源通信。
- CORS。跨源资源分享(Cross-Origin Resource Sharing),允许任何类型请求。 - 简单请求:方法是 - HEAD、- GET、- POST之一,且头信息不超过- Accept等几个字段。否则为非简单请求。
- 如果 ajax 是简单请求,浏览器为请求头添加 - Origin字段表示请求源,如果请求源不在许可范围内,浏览器的响应头不包含- Access-Control-Allow-Origin字段。如果在许可范围内,响应头会包含此字段,并且值为- Origin值。
- 如果ajax是非简单请求,浏览器首先发送预检请求 - OPTIONS,如果服务器确认跨源请求,可以回应- Access-Control-Allow-Origin等字段,浏览器再发出非简单请求,与简单请求类似包含- Origin、响应包含- Access-Control-Allow-Origin。
- 手动设置请求头可以实现跨域: - app.get('/get', (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); // 允许任意源访问 // res.setHeader('Access-Control-Allow-Origin', 'http://www.baidu.com'); // 允许指定源访问 res.send(Date.now().toString()); });1
 2
 3
 4
 5
- 也可以使用 CORS 包。 
 
# 21 Storage 和 cookie
 # 21.1 cookie、localStorage、sessionStorage 的区别
 相同点:
- 存储位置:都是浏览器存储,都存储在浏览器本地。
- 数据共享:都遵循同源原则,sessionStorage还限制是同一页面。
不同点:
- 写入方式:cookie 是服务器端写入的,localStorage和sessionStorage是前端写入的。
- 生命周期:cookie 的生命周期由服务器端在写入时确定,localStorage写入后一直存在(除非手动清除),sessionStorage页面关闭时就被清除。
- 存储大小:cookie 存储空间小(大约 4 KB),localStorage和sessionStorage存储空间大(大约5 MB)。
- 发送请求时是否携带:cookie 在前端给后端发送请求时会自动携带,localStorage和sessionStorage不会。
- 路径限制:cookie 可以限制只属于某个路径下,localStorage和sessionStorage不可以。
- 作用域:cookie 和 localStorage在所有同源窗口共享,sessionStorage不在不同的浏览器窗口共享,即使是同一页面。
- 应用场景:cookie 一般用于存储登录验证信息 Session ID 或 token,localStorage常存储不易变动的数据(减轻服务器压力),sessionStorage检测用户是否刷新页面(如音乐播放器恢复进度条)。
# 21.2 常见问题
- cookie 可以由前端写入吗? 可以,对 - document.cookie赋值。
- token 可以放在 cookie 中吗? 能。 token 一般是用来判断用户是否登录的,它内部包含的信息有:uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)。token 可以存放在 cookie 中,token 是否过期,应该由后端来判断,不该前端来判断,所以 token 存储在 cookie 中只要不设置 cookie 的过期时间就 ok 了,如果 token 失效,就让后端在接口中返回固定的状态表示 token 失效,需要重新登录,再重新登录的时候,重新设置 cookie 中的 token 就行。 
token 认证流程
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端签发一个 token,并把它发送给客户端
- 客户端接收 token 以后会把它存储起来,比如放在 cookie 里或者 localStorage 里
- 客户端每次发送请求时都需要带着服务端签发的 token(把 token 放到 HTTP 的 Header 里)
- 服务端收到请求后,需要验证请求里带有的 token,如验证成功则返回对应的数据
- 如何实现可过期的 - localstorage数据?- localStorage只能用于长久保存整个网站的数据,保存的数据没有过期时间,直到手动去删除。所以要实现可过期的- localStorage缓存的中重点就是:如何清理过期的缓存。目前有两种方法,一种是惰性删除,另一种是定时删除。- 惰性删除是指,某个键值过期后,该键值不会被马上删除,而是等到下次被使用的时候,才会被检查到过期,此时才能得到删除。 - 实现方法是,存储的数据类型是个对象,该对象有两个 - key,一个是要存储的- value值,另一个是当前时间。获取数据的时候,拿到存储的时间和当前时间做对比,如果超过过期时间就清除 cookie。
- 定时删除是指,每隔一段时间执行一次删除操作,并通过限制删除操作执行的次数和频率,来减少删除操作对 CPU 的长期占用。另一方面定时删除也有效的减少了因惰性删除带来的对 - localStorage空间的浪费。- 实现方法:获取所有设置过期时间的 - key判断是否过期,过期就存储到数组中,遍历数组,每隔 1 s(固定时间)删除 5 个(固定个数),直到把数组中的- key从- localstorage中全部删除。
 
# 22 XSS 和 CSRF
XSS:跨站脚本攻击(Cross Site Scripting),向目标网站插入恶意代码,大量用户访问网站时运行恶意脚本获取信息。
CSRF:跨站点请求伪造(Cross Site Request Forgery),盗用用户身份发起请求。
# 22.1 XSS
XSS 是跨站脚本攻击(Cross Site Scripting),不写为 CSS 是为了避免和层叠样式表(Cascading Style Sheets)的缩写混淆,所以将跨站脚本攻击写为 XSS。
攻击者可以通过向 Web 页面里面插入 script 代码,当用户浏览这个页面时,就会运行被插入的 script 代码,达到攻击者的目的。
- XSS 的危害:一般是泄露用户的登录信息 cookie,攻击者可以通过 cookie 绕过登录步骤直接进入站点。
- 获取 cookie:网站中的登录一般都是用 cookie 作为某个用户的身份证明,这是服务器端返回的一串字符。如果 cookie 被攻击者拿到,那么就可以绕过密码登录。当空间、论坛如果可以被插入 script 代码,那么进入空间或者论坛的人的账号就可以轻易被攻击者获取。
- 恶意跳转:直接在页面中插入 window.location.href进行跳转。
 
- XSS 的分类:分为反射型和存储型。
- 反射型 XSS(非持久型 XSS):临时通过 url 访问网站,网站服务端将恶意代码从 url 中取出,拼接在 HTML 中返回给浏览器,用户就会执行恶意代码。
- 存储型 XSS(持久型 XSS):将恶意代码以留言的形式保存在服务器数据库,任何访问网站的人都会受到攻击。
 
- 预防 XSS 攻击的方案:基本是对数据进行严格的输出编码,比如 HTML 元素的编码,JavaScript 编码,CSS 编码,url 编码等等。
- 浏览器的防御和“X-XSS-Protection”有关,默认值为 1,即默认打开 XSS 防御,可以防御反射型的 XSS,不过作用有限,只能防御注入到 HTML 的节点内容或属性的 XSS,例如 URL 参数中包含 script 标签。不建议只依赖此防御手段。
- 防御 HTML 节点内容,通过转义 <为<以及>为>来实现防御 HTML 节点内容。
- 预防 HTML 属性,通过转义 "为&quto;来实现防御,一般不转义空格,但是这要求属性必须带引号。
- 预防 JavaScript 代码,通过将数据进行 JSON 序列化。
- 防御富文本是比较复杂的工程,因为富文本可以包含 HTML 和 script,这些难以预测与防御,建议是通过白名单的方式来过滤允许的 HTML 标签和标签的属性来进行防御,大概的实现方式是:
- 将 HTML 代码段转成树级结构的数据
- 遍历树的每一个节点,过滤节点的类型和属性,或进行特殊处理
- 处理完成后,将树级结构转化成 HTML 代码
- 开启浏览器 XSS 防御:Http Only cookie,禁止 JavaScript 读取某些敏感 cookie,攻击者完成 XSS 注入后也无法窃取此 cookie
 
 
# 22.2 CSRF
CSRF 是跨站点请求伪造(Cross Site Request Forgery),和 XSS 攻击一样,有巨大的危害性,就是攻击者盗用了用户的身份,以用户的身份发送恶意请求,但是对服务器来说这个请求是合理的,这样就完成了攻击者的目标。
- CSRF 攻击的过程原理: - 用户打开浏览器,访问目标网站 A,输入用户名和密码请求登录。
- 用户信息在通过认证后,网站 A 产生一个 cookie 信息返回给浏览器,这个时候用户以可正常发送请求到网站 A。
- 用户在没有退出网站 A 之前在同一个浏览器打开了另一个新网站 B。
- 新网站 B 收到用户请求之后返回一些攻击代码,并发出一个请求要求访问返回 cookie 的网站 A。
- 浏览器收到这些攻击性代码之后根据新网站 B 的请求在用户不知道的情况下以用户的权限操作了 cookie 并向网站A服务器发起了合法的请求。
 
- 预防 CSRF 攻击的主要策略: - 使用验证码,在表单中添加一个随机的数字或者字母验证码,强制要求用户和应用进行直接的交互。 
- HTTP 中 Referer 字段,检查是不是从正确的域名访问过来,它记录了 HTTP 请求的来源地址。 - 验证 HTTP Referer 字段的好处就是实施起来特别简单,普通的网站开发不需要特别担心 CSRF 漏洞,只需要在最后面设置一个拦截器来验证 referer 的值就可以了,不需要改变已有的代码逻辑,非常便捷。但是这个方法也不是万无一失的,虽然 referer 是浏览器提供的,但是不同的浏览器可能在 referer 的实现上或多或少有自身的漏洞,所以使用 referer 的安全保证是通过浏览器实现的。 
- 使用 token 验证,在 HTTP 请求头中添加 token 字段,并且在服务器端建立一个拦截器验证这个 token,如果 token 不对,就拒绝这个请求。 - 使用 token 验证的方法要比 referer 更安全一些,需要把 token 放在一个 HTTP 自定义的请求头部中,解决了使用 get 或者 post 传参的不便性。 
 
# 23 JS 事件流
事件流:从页面中接收事件的顺序,即事件在页面中传播的顺序。
# 23.1 事件捕获与事件冒泡
微软认为事件发生的顺序应该是事件冒泡(bubbling),即由内向外;网景认为事件发生的顺序应该是事件捕获(capturing),即由外向内。W3C 采取折中方式:先捕获后冒泡。
- onclick:绑定的- onclick()事件在冒泡阶段执行,例如:- <body> <div onclick="divClick()"> <p onclick="pClick()"> <span onclick="spanClick()">点击事件</span> </p> </div> </body> <script> function divClick() { console.log('divClick'); } function pClick() { console.log('pClick'); } function spanClick() { console.log('spanClick'); } </script>1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18- 点击后,输出顺序为 - spanClick、- pClick、- divClick(冒泡顺序)。
- stopPropagation():要阻止事件冒泡,可以使用- stopPropagation()函数:- function pClick(event) { console.log('pClick'); event.stopPropagation(); }1
 2
 3
 4
- addEventListener:接收三个参数,分别是- event(事件名)、- function、- useCapture。第三个参数默认为- false,如果设置为- true则表示事件在捕获阶段执行,例如:- <body> <div id="div"> <span id="span">点击事件</span> </div> </body> <script> const div = document.getElementById('div'); const span = document.getElementById('span'); div.addEventListener('click', function(e) { console.log('div bubbling'); }, false); span.addEventListener('click', function(e) { console.log('span bubbling'); }, false); div.addEventListener('click', function(e) { console.log('div capturing'); }, true); span.addEventListener('click', function(e) { console.log('span capturing'); }, true); </script>1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21- 点击后,输出顺序为 - div capturing、- span capturing、- span bubbling、- div bubbling。
# 23.2 事件代理
由于事件 event 的 target 为最深层的元素,我们可以将一组元素的事件绑定到他的外层元素上,这样事件会在冒泡阶段被传递到外层函数上执行。好处是可以同时给父元素的多个子元素绑定事件,节省开销。
<ul id="list">
  <li>red</li>
  <li>yellow</li>
</ul>
</body>
<script>
  const list = document.getElementById('list');
  list.addEventListener('click', e => {
    const li = e.target;
    if (li.tagName.toLowerCase() === 'li') {
      console.log("Color is " + li.textContent);
    }
  });
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
这样点击哪个 li 就输出对应的颜色。
# 23.3 IE兼容性
IE9以下不兼容 addEventListener,可以使用 attachEvent(event, function) 函数。
它在冒泡阶段调用,并且事件名要加上 "on" 前缀。
# 24 Ajax
Ajax(Asynchronous Javascript And XML)是异步 JavaScript 和 XML。使用 Ajax 技术网页应用能够快速地将增量更新呈现在用户界面上,而不需要重载(刷新)整个页面,这使得程序能够更快地回应用户的操作。
XML(Extensible Markup Language)是可扩展标记语言,可以用来标记数据、定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言。
XHR(XMLHttpRequest)是 XML HTTP 请求,用于与服务器交互数据,是 Ajax 功能实现所依赖的对象。
# 24.1 Ajax的创建过程
- 创建XHR对象: - new XMLHttpRequest()
- 设置请求参数: - request.open(Method, 服务器接口地址);
- 发送请求: - request.send(),如果是- get请求不需要参数,- post请求需要参数- request.send(data)及请求头
- 监听请求成功后的状态变化:根据状态码进行相应的处理 - XHR.onreadystatechange = function () { if (XHR.readyState === 4 && XHR.status === 200) { console.log(XHR.responseText); // 主动释放,JS 本身也会回收的 XHR = null; } };1
 2
 3
 4
 5
readyState 值说明:
- 0:初始化,XHR 对象已经创建,还未执行 open
- 1:载入,已经调用 open方法,但是还没发送请求
- 2:载入完成,请求已经发送完成
- 3:交互,可以接收到部分数据
- 4:数据全部返回
status 值说明:
- 200:成功
- 404:没有发现文件、查询或 URL
- 500:服务器产生内部错误
# 24.2 Ajax 的原理及优缺点
Ajax 的原理:相当于在用户和服务器之间加一个中间层(Ajax 引擎),使用户操作与服务器响应异步化。由客户端请求 Ajax 引擎,再由 Ajax 引擎请求服务器,服务器作出一系列响应之后返回给 Ajax 引擎,由 Ajax 引擎决定将这个结果写入到客户端的什么位置。实现页面无刷新更新数据。
- 优点:在不刷新整个页面的前提下与服务器通信维护数据。不会导致页面的重载可以把前端服务器的任务转嫁到客服端来处理,减轻服务器负担,节省带宽。
- 劣势:不支持返回上一次请求内容。对搜索引擎的支持比较弱(百度在国内搜索引擎的占有率最高,但是很不幸,它并不支持 Ajax 数据的爬取);不容易调试。
怎么解决呢?通过 location.hash 值来解决 Ajax 过程中导致的浏览器前进后退按键失效。
解决以前被人常遇到的重复加载的问题。主要比较前后的 hash 值,看其是否相等,在判断是否触发 Ajax。
function getData(url) {
    var xhr = new XMLHttpRequest();  // 创建一个对象,创建一个异步调用的对象
    xhr.open('get', url, true)  // 设置一个 HTTP 请求,设置请求的方式,URL 以及验证身份
    xhr.send() //发送一个 HTTP 请求
    xhr.onreadystatechange = function () {  //设置一个 HTTP 请求状态的函数
        if (xhr.readyState == 4 && xhr.status ==200) {
            console.log(xhr.responseText)  // 获取异步调用返回的数据
        }
    }
}
2
3
4
5
6
7
8
9
10
- 适用场景:表单驱动的交互、深层次的树的导航、快速用户间响应、投票等场景、数据过滤和操纵场景、普通的文本输入场景
- 不适用场景:部门简单表单、搜索、基本导航、替换大量文本、对呈现的操纵
# 24.3 Ajax 和 Axios 的区别
Axios 是一个基于 Promise 的 HTTP 库,主要实现 Ajax 异步通信功能,用于向后端发起请求,还有在请求中做更多是可控功能。Axios 实现对 Ajax 的封装。
简单理解为:封装好的、基于 Promise 的发送请求的方法。
Axios 是通过 Promise 实现对 Ajax 技术的一种封装,就像 jQuery 对 Ajax 的封装一样,Axios 回来的数据是 Promise,Ajax 回来的数据是回调,Axios 比 Ajax 更好用更安全。
简单来说就是 Ajax 技术实现了局部数据的刷新,Axios 实现了对 Ajax 的封装;Axios 有的 Ajax 都有,Ajax 有的 Axios 不一定有。Axios 是 Ajax,Ajax 不止 Axios。
参考:
# 25 setTimeout 和 setInterval
 setTimeout 用于延时执行一个函数,传入参数为 fn(回调函数)、timeout(延时时间)、...args(执行参数)。
setInterval 用于开启一个不断执行的计时器,传入参数为 fn(回调函数)、timeout(间隔时间)、...args(执行参数)。
这两者都是 宏任务,并在当前所有宏任务、微任务结束后执行。
# 25.1 返回值问题
浏览器下,这两者返回的是一个唯一 id,通过 clearTimeout 或 clearInterval 可以清除。
node 下,这两者返回的是一个 Timeout 对象,同样通过 clearTimeout 或 clearInterval 可以清除。
值得注意的是,如果把 setTimeout 或 setInterval 的返回值赋给一个变量,那么清除延时器或定时器后,变量的值不变。如果需要判断变量的值(如防抖、节流场景),需要清除后主动将变量值设为 null。
# 25.2 用setTimeout实现setInterval
 function mySetInterval(callback, timeout, ...arguments) {
  const fn = () => {
    callback(...arguments);
    setTimeout(fn, timeout);
  }
  setTimeout(fn, timeout);
}
mySetInterval(() => {
  console.log('hello');
}, 1000);
2
3
4
5
6
7
8
9
10
11
原理:在 fn 内递归调用 fn,使 callback 一直执行。
扩展:如何实现 clearInterval?
var timeWorker = {};
function mySetInterval(callback, timeout, ...arguments) {
  var key = Symbol();
  const fn = () => {
    callback(...arguments);
    timeWorker[key] = setTimeout(fn, timeout);
  }
  timeWorker[key] = setTimeout(fn, timeout);
  return key;
}
function myClearInterval(key) {
  if (key in timeWorker) {
    clearTimeout(timeWorker[key]);
    delete timeWorker[key];
  }
}
const interval =  mySetInterval(() => {
  console.log('hello');
}, 1000);
setTimeout(() => {
  myClearInterval(interval);
}, 5500);
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
原理:因为 setTimeout 返回一个唯一 id(浏览器下,node 下是一个对象),我们可以模拟这个过程,用一个唯一 id(比如 Symbol)记录下最后一个定时器并记录到一个全局变量中。清除定时器时,取出最后一个定时器将其清除。
# 26 事件循环
JS 的并发模型基于事件循环(Event Loop),不同于 C、Java 等语言。
一个 JS 运行时包含了一个待处理的消息队列,每一个消息都关联着一个用于处理这个消息的函数。
在事件循环期间的某个时刻,运行时从最先进入队列的消息开始处理队列中的消息,为此,这个消息会被移除队列,并作为输入参数调用与之关联的函数。调用一个函数会为其创造一个新的栈帧。函数的处理会一直进行到执行栈再次为空为止,然后事件循环会处理队列中的下一个消息(如果还有)。
下面代码输出什么?
const ms = new Date().getTime();
setTimeout(() => {
  console.log("Ran after " + (new Date().getTime() - ms) + " ms");
}, 500);
while (true) {
  if (new Date().getTime() - ms >= 2000) {
    console.log("Good, looped for 2 second");
    break;
  }
}
2
3
4
5
6
7
8
9
10
11
12
Good, looped for 2 second
Ran after 2006 ms
2
第3行,setTimeout 语句进入调用栈,这是一个延时器,因此将回调函数(第 4 行)加入 Web API,Web API 的作用是在 500 ms 后将回调函数的执行加入到宏任务队列。
紧接着执行 while 循环,500 ms 后 Web API 将回调函数的执行加入宏任务队列,但此时 while 循环仍在执行,调用栈未被清空,直到 2000 ms 后 while 循环退出,调用栈清空,取出宏任务队列中的回调函数并加入调用栈执行。
事件循环可以理解为以下循环:
- 主线程执行同步代码,执行过程中产生调用栈,如果有异步事件,则交给异步模块处理,如异步任务有结果,则异步模块在消息队列添加消息
- 如果同步任务完成(调用栈清空),主线程检查消息队列,如果消息队列不为空,取出头部的待处理消息,加入主线程
- 主线程重复以上过程
参考:
# 27 JS设计模式
待补充
# 27.1 单例
# 27.2 手写 EventEmitter
 手写一个事件派发器,统一管理某一类型的回调,并在之后对特定类型进行触发,注意每一类型可能不止一个事件。
class EventEmitter {
  constructor() {
    this.listeners = {};
  }
  /**
   * 注册事件
   * @param {string} type 事件类型
   * @param {function} fn 事件处理函数
   */
  on(type, fn) {
    if (!this.listeners[type]) {
      this.listeners[type] = [];
    }
    this.listeners[type].push(fn);
  }
  /**
   * 发布事件
   * @param {string} type 事件类型
   * @param  {...any} args 事件参数
   */
  emit(type, ...args) {
    if (this.listeners[type]) {
      this.listeners[type].forEach(fn => fn(...args));
    }
  }
  /**
   * 移除某个类型的一个事件
   * @param {string} type 事件类型
   * @param {function} fn 事件处理函数
   */
  off(type, fn) {
    if (this.listeners[type]) {
      this.listeners[type] = this.listeners[type].filter(item => item !== fn);
      if (this.listeners[type].length === 0) {
        delete this.listeners[type];
      }
    }
  }
  /**
   * 移除某个类型的所有事件
   * @param {string} type 事件类型
   */
  offAll(type) {
    if (this.listeners[type]) {
      delete this.listeners[type];
    }
  }
}
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
使用示例:
const ee = new EventEmitter();
ee.on('imagine', () => console.log('imagine'));
ee.emit('imagine'); // imagine
ee.on('imagine', (name, address) => console.log(`Hello, I am ${name} from ${address}`));
ee.emit('imagine', 'Jack', 'China');  // imagine  Hello, I am Jack from China
const fn = () => console.log('I am fn');
ee.on('TestOff', fn);
ee.emit('TestOff'); // I am fn
ee.off('TestOff', fn);
ee.emit('TestOff'); // nothing
ee.offAll('imagine');
console.log(ee);  // EventEmitter { listeners: {} }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
