JavaScript 重难点实例精讲

21h

周雄

第 1 章 JavaScript 重点概念

是 4 种常见的出现 undefined 的场

是 3 种常见的出现 null 的场景。

在需要将两者转换成对象时,都会抛出一个 TypeError 的异常

1.2.2 Number 类型转换

如果值为对象类型,则会先调用对象的 valueOf()函数获取返回值,并将返回值按照上述步骤重新判断能否转换为 Number 类型。如果都不满足,则会调用对象的 toString()函数获取返回值,并将返回值重新按照步骤判断能否转换成 Number 类型。如果也不满足,则返回“NaN”。

如果 toString()函数和 valueOf()函数返回的都是对象类型而无法转换成基本数据类型,则会抛出类型转换的异常。

好吧 TS 真香

parseInt(0x12, 16); // 24

parseInt(0x12, 16); // 24

任何整数以 0 为基数取整时,都会返回本身,所以第一行代码会返回“1”。

parseFloat('4e3'); // 4000parseInt('4e3', 10); // 4

1.2.3 isNaN()函数与 Number.isNaN()函数对比

isNaN()函数正好会进行数据的类型

isNaN()函数正好会进行数据的类型转换

isNaN(new Date()); // falseisNaN(new Date().toString()); // true

// 兼容性处理 if(!Number.isNaN) { Number.isNaN = function (n) { return n !== n; }}

1.2.4 浮点型运算

0.1 + 0.2 = 0.300000000000000040.7 + 0.1 = 0.7999999999999999// 减法 1.5 - 1.2 = 0.300000000000000040.3 - 0.2 = 0.09999999999999998// 乘法 0.7 _ 180 = 125.999999999999999.7 _ 100 = 969.9999999999999// 除法 0.3 / 0.1 = 2.99999999999999960.69 / 10 = 0.06899999999999999

因为浮点型数使用 64 位存储时,最多只能存储 52 位的小数位,对于一些存在无限循环的小数位浮点数,会截取前 52 位,从而丢失精度,

1.3.1 String 类型的定义与调用

基本字符串在作比较时,只需要比较字符串的值即可;而在比较字符串对象时,比较的是对象所在的地址。

var s1 = '2 + 2'; // 创建一个字符串字面量 var s2 = new String('2 + 2'); // 创建一个对象字符串 console.log(eval(s1)); // 4console.log(eval(s2)); // String {"2 + 2"}

1.3.2 String 类型常见算法

  1. 字符串逆序输出

  2. 统计字符串中出现次数最多的字符及出现的次数

  3. 去除字符串中重复的字符

return Array.prototype.filter.call(str, (char, index, arr) => arr.indexOf (char) === index).join('');

  1. 判断一个字符串是否为回文字符串

1.4.2 typeof 运算符

所有构造函数通过 new 操作符实例化后得到的对象,例如 new Date()、new function(){},但是 new Function(){}除外

技术角度讲,函数在 ECMAScript 中是对象,不是一种数据类型。然而,函数也确实有一些特殊的属性,因此通过 typeof 运算符来区分函数和其他对象是有必要的。

typeof 1 / 0; // "NaN"typeof (1 / 0); // "number"

1.4.3 逗号运算符

可以作为一个运算符,作用是将多个表达式连接起来,从左至右依次执行。

x = 8 _ 2, x _ 4

  1. 在 for 循环中批量执行表达式

  2. 用于交换变量,无须额外变量

var a = 'a';var b = 'b';// 方案 1a = [b, b = a][0];// 方案 2a = [b][b = a, 0];

逗号运算符可以使多个表达式先后执行,并且返回最后一个表达式的值,

1.4.4 运算符优先级

(var a = 1); // SyntaxError: Unexpected token var

1.5 toString()函数与 valueOf()函数

valueOf()函数的作用是返回最适合引用类型的原始值,如果没有原始值,则会返回引用类型自身。

用于数据运算,则会优先

调用 valueOf()函数

数据展示,则会优先调用 toString()函数

[] == 0; // true[1] == 1; // true[2] == 2; // true

var obj = { i: 10, toString: function () { console.log('toString'); return this.i; }, valueOf: function () { console.log('valueOf'); return this.i; }};

+obj; // valueOf'' + obj; // valueOfString(obj); // toStringNumber(obj); // valueOfobj == '10'; // valueOf,trueobj === '10'; // fals

e

1.6 JavaScript 中常用的判空方法

if(obj == null) {} // 可以判断 null 或者 undefinedif(obj === undefined) {} // 只能判断 undefine

d

// 判断变量为空 function isEmpty(obj) { for(let key in obj) { if(obj.hasOwnProperty(key)) { return false; } } return true;}

arr instanceof Array && arr.length === 0

str == '' || str.trim().length == 0;

!x == true 的所有情况

· 变量为 null。 · 变量为 undefined。 · 变量为空字符串' '。 · 变量为数字 0,包括+0、-0。 · 变量为 NaN

1.7 JavaScript 中的 switch 语句

JavaScript 中,switch 语句可以用来判断任何类型的值,不一定是 Number 类型。

JavaScript 中对于 case 的比较是采用严格相等(===)的

第 2 章 引用数据类型

JavaScript 中常用的引用数据类型包括 Object 类型、Array 类型、Date 类型、RegExp 类型、Math 类型、Function 类型以及基本数据类型的包装类型,如 Number 类型、String 类型、Boolean 类型等。

2.1.1 深入了解 JavaScript 中的 new 操作符

如果函数没有 return 值,则默认 return this

function New() { var obj = {}; obj._ proto _ = Cat.prototype; // 核心代码,用于继承 var res = Cat.apply(obj, arguments); return typeof res === 'object' ? res : obj;}

2.1.2 Object 类型的实例函数

propertyIsEnumerable(propertyName

console.log(array.propertyIsEnumerable('length')); // false :length 属性继承自 Array 类型 console.log(array.propertyIsEnumerable('toString')); // false :toString()函数 // 继承自 Object

console.log(a.propertyIsEnumerable('name')); // false :name 设置为不可枚举

2.1.3 Object 类型的静态函数

Object.create = function (proto, propertiesObject) { // 省去中间的很多判断 function F() {} F.prototype = proto; return new F();};

Object.getOwnPropertyNames()函数

获取对象的所有实例属性和函数,不包含原型链继承的属性和函数,数据格式为数组。

Object.keys()函数

keys()函数区别于 getOwnPropertyNames()函数的地方在于,keys()函数只获取可枚举类型的属性。

2.2.1 判断一个变量是数组还是对象

instanceof 运算符用于通过查找原型链来检测某个变量是否为某个类型数据的实例

instanceof 运算符存在一定的缺陷,这点在 4.6 节中

var a = [1, 2, 3];

console.log(a.constructor === Array); // trueconsole.log(a.constructor === Object); // false

var a = [1, 2, 3];var b = {name: 'kingx'};console.log(Object.prototype.toString.call(a)); // [object Array]

console.log(Object.prototype.toString.call(b)); // [object Object]

// 鲜为人知的事实:其实 Array.prototype 也是一个数组。Array.isArray(Array.prototype);

2.2.2 filter()函数过滤满足条件的数据

filter()函数,还有很重要的功能是可以进行数组去重,这一点会专门在 2.2.6 小节中

2.2.3 reduce()函数累加器处理数组元素

  1. 求数组每个元素相加的和

  2. 统计数组中每个元素出现的次数

  3. 多维度统计数据

2.2.4 求数组的最大值和最小值

这个场景,我们总结出了 6 种算法,

apply()函数传入的第一个值为{},实际表示当前执行环境的全局对象。

设置为 null、undefined 或{}都会得到

相同的效果

2.2.5 数组遍历的 7 种方法及兼容性处理(polyfill)

// forEach()函数兼容性处理
Array.prototype.forEach =
  Array.prototype.forEach ||
  function (fn, context) {
    for (var k = 0, length = this.length; k < length; k++) {
      if (
        typeof fn === "function" &&
        Object.prototype.hasOwnProperty.call(this, k)
      ) {
        fn.call(context, this[k], k, this);
      }
    }
  };
// map()函数兼容性处理
Array.prototype.map =
  Array.prototype.map ||
  function (fn, context) {
    var arr = [];
    if (typeof fn === "function") {
      for (var k = 0, length = this.length; k < length; k++) {
        if (
          typeof fn === "function" &&
          Object.prototype.hasOwnProperty.call(this, k)
        ) {
          arr.push(fn.call(context, this[k], k, this));
        }
      }
    }
    return arr;
  };
// filter()函数兼容性处理
Array.prototype.filter =
  Array.prototype.filter ||
  function (fn, context) {
    var arr = [];
    if (typeof fn === "function") {
      for (var k = 0, length = this.length; k < length; k++) {
        if (
          typeof fn === "function" &&
          Object.prototype.hasOwnProperty.call(this, k)
        ) {
          fn.call(context, this[k], k, this) && arr.push(this[k]);
        }
      }
    }
    return arr;
  };
// some()函数兼容性处理
Array.prototype.some =
  Array.prototype.some ||
  function (fn, context) {
    var passed = false;
    if (
      typeof fn === "function" &&
      Object.prototype.hasOwnProperty.call(this, k)
    ) {
      for (var k = 0, length = this.length; k < length; k++) {
        if (passed === true) break;
        // 如果有返回值为“true”,直接跳出循环
        passed = !!fn.call(context, this[k], k, this);
      }
    }
    return passed;
  };
// every()函数兼容性处理
Array.prototype.every =
  Array.prototype.every ||
  function (fn, context) {
    var passed = true;
    if (
      typeof fn === "function" &&
      Object.prototype.hasOwnProperty.call(this, k)
    ) {
      for (var k = 0, length = this.length; k < length; k++) {
        if (passed === false) break;
        // 如果有返回值为“false”,直接跳出循环
        passed = !!fn.call(context, this[k], k, this);
      }
    }
    return passed;
  };
// reduce()函数兼容性处理
Array.prototype.reduce =
  Array.prototype.reduce ||
  function (callback, initialValue) {
    var previous = initialValue,
      k = 0,
      length = this.length;
    if (typeof initialValue === "undefined") {
      previous = this[0];
      k = 1;
    }
    if (typeof callback === "function") {
      for (k; k < length; k++) {
        //每轮计算完后,需要将计算后的返回值重新赋给累加函数的第一个参数
        this.hasOwnProperty(k) &&
          (previous = callback(previous, this[k], k, this));
      }
    }
    return previous;
  };
// find()函数兼容性处理
Array.prototype.find =
  Array.prototype.find ||
  function (fn, context) {
    if (typeof fn === "function") {
      for (var k = 0, length = this.length; k < length; k++) {
        if (fn.call(context, this[k], k, this)) {
          return this[k];
        }
      }
    }
    return undefined;
  };

2.2.6 数组去重的 7 种算法

function arrayUnique6(array) {
  return Array.from(new Set(array));
}

2.2.7 找出数组中出现次数最多的元素

  1. 利用键值对

  2. 借助 Array 类型的 reduce()函数

  3. 借助 ES6 与逗号运算符进行代码优化

Array.prototype.getMost4 = function () { var obj = this.reduce((p, n) =>

(p[n]++ || (p[n] = 1), (p.max = p.max >= p[n] ? p.max : p[n]), (p.key = p.max > p[n] ? p.key : n), p), {}); return '次数最多的元素为:' + obj.key + ',次数为:' + obj.max;

}

3.1.1 函数的定义

// 具有函数名的函数表达式 var sum = function foo(num1, num2) { return num1 + num2;}

其中 foo 是函数名称,它实际是函数内部的一个局部变量,在函数外部是无法直接调用的,示例如下。

console.log(foo(1, 3)); // ReferenceError: foo is not defined

使用 Function()构造函数创建的函数,并不遵循典型的作用域,它将一直作为顶级函数执行。所以在一个函数 A 内部调用 Function()构造函数时,其中的函数体并不能访问到函数 A 中的局部变量,而只能访问到全局变量。

3.1.2 函数的调用

JavaScript 解释器在解析语句时,会将 function 关键字当作函数声明的开始,函数的声明是需要有函数名称的,而上面的代码却并没有函数名称,所以会抛出语法异常。

第一个是让小括号前面的语句是一段函数表达式,则函数表达式后面跟的小括号表示的是函数立即执行。

var sum = function (num1, num2) { return num1 + num2;}(1, 2);console.log(sum); // 3

第二个是使用小括号将整个语句全部括起来,当作一个完整的函数表达式调用。

3.1.3 自执行函数

自执行函数的多种表现形式

3.2 函数参数

function fn1() { var param = 'hello'; fn2(param); console.log(arg); // 在主调函数中不能访问到形参 arg,会抛出异常 }function fn2(arg) {

console.log(arg); // 在函数体内能访问到形参 arg,输出“hello” console.log(param); // 在函数体内不能访问到实参 param,会抛出异常 }fn1()

实际是将实参的内存地址传递给形参,即实参和形参都指向相同的内存地址,此时形参可以修改实

参的值,但是不能修改实参的内存地址

var arg = {name: 'kingx'};function fn(param) { param.name = 'kingx2'; param = {};}fn(arg);console.log(arg); // {name: "kingx2"}

3.2.2 arguments 对象的性质

function foo(a, b, c) { console.log(arguments.length); // 2 arguments[0] = 11; console.log(a); // 11 b = 12; console.log(arguments[1]); // 12 arguments[2] = 3; console.log(c); // undefined c = 13; console.log(arguments[2]); // 3 console.log(arguments.length); // 2}foo(1, 2)

arguments 对象有一个很特殊的属性 callee,表示的是当前正在执行的函数,在比较时是严格相等的。

使用 arguments.callee 属性后会改变函数内部的 this 值。

3.2.3 arguments 对象的应用

// 通用求和函数 function sum() { // 通过 call()函数间接调用数组的 slice()函数得到函数参数的数组 var arr = Array.prototype.slice.call(arguments); // 调用数组的 reduce()函数进行多个值的求和 return arr.reduce(function (pre, cur) { return pre + cur; }, 0)}sum(1, 2); // 3sum(1, 2, 3); // 6sum(1, 2, 3, 4); // 1

3.3 构造函数

在函数体内部使用 this 关键字,表示要生成的对象实例,构造函数并不会显式地返回任何值,而是默认返回“this”

3.4.2 变量提升

需要注意的一点是,会产生提升的变量必须是通过 var 关键字定义的,而不通过 var 关键字定义的全局变量是不会产生变量提升的。

3.5.2 闭包的概念

官方有一个通用的解释:一个拥有许多变量和绑定了这些变量执行上下文环境的表达式,通常是一个函数。

闭包有两个很明显的特点。 · 函数拥有的外部变量的引用,在函数返回时,该变量仍然处于活跃状态。 · 闭包作为一个函数返回时,其执行上下文环境不会被销毁,仍处于执行上下文环境中。

3.5.3 闭包的用途

  1. 结果缓存

  2. 封装

  3. 多个相同函数名问题

3.6 this 使用详解

bind()函数在改变函数的执行主体后,并没有立即调用,而是可以在任何时候调用,

处理 DOM 事件处理程序中的 this 时

bind()函数显得尤为有用

var user = { data: [ {name: "kingx1", age: 11}, {name: "kingx2", age: 12} ], clickHandler: function (event) { // 随机生成整数 0 或 1 var randomNum = ((Math.random() _ 2 | 0) + 1) - 1; // 从 data 数组里随机获取 name 属性和 age 属性,并输出 console.log(this.data[randomNum].name + " " + this.data[randomNum].age); }};var button = document.getElementById('btn');button.onclick = user.clickHandler;

function f(k) { this.m = k; return this;}var m = f(1);var n = f(2);console.log(m.m);console.log(n.m)

4.1 对象的属性和访问方式

对象的属性可以分为数据属性和访问器属性。

[[Configurable]]、[[Enumerable]]、[[Writable]]特性值都为 true,[[Value]]

[[Configurable]]、[[Enumerable]]、[[Get]]和[[Set]]。

4.1.2 属性的访问方式

第一点,点操作符是静态的,只能是一个以属性名称命名的简单描述符,而且无法修改;而中括号操作符是动态的,可以传递字符串或者变量,并且支持在运行时修改。

第二点,点操作符不能以数字作为属性名,而中括号操作符可以。

如果属性名中包含会导致语法错误的字符,或者属性名中含有关键字或者保留字,可以使用方括号操作符

4.2 创建对象

在 JavaScript 中,对象是一系列无序属性的集合,属性值可以为基本数据类型、对象或者函数,因此对象实际就是一组键值对的组合。

JavaScript 中创建对象的 7 种方式。

  1. 基于 Object()构造函数

  2. 基于对象字面量

  3. 基于工厂方法模式

但是创建的所有实例都是 Object 类型,无法更进一步区分具体的类型。

  1. 基于构造函数模式

  2. 基于原型对象的模式

  3. 构造函数和原型混合的模式

  4. 基于动态原型模式

动态原型模式是将原型对象放在构造函数内部,通过变量进行控制,只在第一次生成实例的时候进行原型的设置。 动态原型的模式相当于懒汉模式,只在生成实例时设置原型对象,但是功能与构造函数和原型混合模式是相同的。

4.3 对象克隆

引用数据类型如果执行的是浅克隆,对克隆后值的修改会影响到原始值;如果执行的是深克隆,则克隆的对象和原始对象相互独立,不会彼此影响。

4.3.1 对象浅克隆

// JavaScript 实现对象浅克隆——引用复制
function shallowClone(origin) {
  var result = {};
  // 遍历最外层属性
  for (var key in origin) {
    // 判断是否是对象自身的属性
    if (origin.hasOwnProperty(key)) {
      result[key] = origin[key];
    }
  }
  return result;
}
  1. ES6 的 Object.assign()函数

4.3.2 对象深克隆

  1. JSON 序列化和反序列化

· 无法实现对函数、RegExp 等特殊对象的克隆。 · 对象的 constructor 会被抛弃,所有的构造函数会指向 Object,原型链关系断裂。 · 对象中如果存在循环引用,会抛出异常

循环引用的结构无法序列化成 JSON 字符串”。

// \__ _ 类型判断 \_/
(function (_) {
  // 列举出可能存在的数据类型
  var types =
    "Array Object String Date RegExp Function Boolean Number Null Undefined".split(
      ""
    );

  function type() {
    // 通过调用 toString()函数,从索引为 8 时截取字符串,得到数据类型的值
    return Object.prototype.toString.call(this).slice(8, -1);
  }
  for (var i = types.length; i--; ) {
    _["is" + types[i]] = (function (self) {
      return function (elem) {
        return type.call(elem) === self;
      };
    })(types[i]);
  }
  return _;
})((_ = {}));
//\__ _ 深克隆实现方案 \_ @param source 待克隆的对象
//- @returns {_} 返回克隆后的对象 _/
function deepClone(source) {
  // 维护两个储存循环引用的数组
  var parents = [];
  var children = [];
  // 用于获得正则表达式的修饰符,/igm
  function getRegExp(reg) {
    var result = "";
    if (reg.ignoreCase) {
      result += "i";
    }
    if (reg.global) {
      result += "g";
    }
    if (reg.multiline) {
      result += "m";
    }
    return result;
  }
  // 便于递归的_clone()函数
  function _clone(parent) {
    if (parent === null) return null;
    if (typeof parent !== "object") return parent;
    var child, proto;
    // 对数组做特殊处理
    if (_.isArray(parent)) {
      child = [];
    } else if (_.isRegExp(parent)) {
      // 对正则对象做特殊处理
      child = new RegExp(parent.source, getRegExp(parent));
      if (parent.lastIndex) child.lastIndex = parent.lastIndex;
    } else if (_.isDate(parent)) {
      // 对 Date 对象做特殊处理
      child = new Date(parent.getTime());
    } else {
      // 处理对象原型
      proto = Object.getPrototypeOf(parent); // 利用 Object.create 切断原型链
      child = Object.create(proto);
    } // 处理循环引用
    var index = parents.indexOf(parent);
    if (index !== -1) {
      // 如果父数组存在本对象,说明之前已经被引用过,直接返回此对象
      return children[index];
    }
    // 没有引用过,则添加至 parents 和 children 数组中
    parents.push(parent);
    children.push(child); // 遍历对象属性
    for (var prop in parent) {
      if (parent.hasOwnProperty(prop)) {
        // 递归处理
        child[prop] = _clone(parent[prop]);
      }
    }
    return child;
  }
  return _clone(source);
}

4.4.1 原型对象、构造函数、实例之间的关系

· 原型对象、构造函数和实例之间的关系是什么样的? · 使用原型对象创建了对象的实例后,实例的属性读取顺序是什么样的? · 假如重写了原型对象,会带来什么样的问题

将一个对象字面量赋给 prototype 属性的方式实际是重写了原型对象,等同于切断了构造函数和最初原型之间的关系。因此有一点需要注意的是,如果仍然想使用 constructor 属性做后续处理,则应该在对象字面量中增加一个 constructor 属性,指向构造函数本身,否则原型的 constructor 属性会指向 Object 类型的构造函数,从而导致 constructor 属性与构造函数的脱离。

如果在重写原型对象之前,已经生成了对象的实例,则该实例将无法访问到新的原型对象中的函数。

4.5.1 原型链继承

4.5.1 原型链继承

重写子类的 prototype 属性,将其指向父类的实例。

4.5.2 构造继承

4.5.2 构造继承

4.5.3 复制继承

4.5.3 复制继承

4.5.4 组合继承

组合继承

4.5.5 寄生组合继承

寄生组合继承

4.6 instanceof 运算符

构造函数 constructor()的 prototype 属性是否出现在 target 对象的原型链中

4.6.3 instanceof 运算符的复杂用法

function instanceOf(L, R) {
  var O = R.prototype;
  // 取 R 的显示原型
  L = L.__proto__; // 取 L 的隐式原型
  while (true) {
    if (L === null) return false;
    if (O === L)
      // 这里是重点:当 O 严格等于 L 时,返回“true”
      return true;
    L = L.__proto__; // 如果不相等则重新取 L 的隐式原型
  }
}

5.2 HTMLCollection 对象与 NodeList 对象

children 属性和 childNodes 属性的不同在本质上是 HTMLCollection 对象和 NodeList 对象的不同。HTMLCollection 对象与 NodeList 对象都是 DOM 节点的集合,但是在节点处理方式上是有差异的。

HTMLCollection 对象和 NodeList 对象并不是历史文档状态的静态快照,而是具有实时性的

HTMLCollection 对象与 NodeList 对象都只是类数组结构,并不能直接调用数组的函数。而通过 call()函数和 apply()函数处理为真正的数组后,它们就转变为一个真正的静态值了,不会再动态反映 DOM 的变化。

NodeList 对象与 HTMLCollection 对象相比,存在一些细微的差异,主要表现在不是所有的函数获取的 NodeList 对象都是实时的。例如通过 querySelectorAll()函数获取的 NodeList 对象就不是实时的。

  • (1)相同点
  1. 都是类数组对象,有 length 属性,可以通过 call()函数或 apply()函数处理成真正的数组。
  2. 都有 item()函数,通过索引定位元素。
  3. 都是实时性的,DOM 树的变化会及时反映到 HTMLCollection 对象和 NodeList 对象上,只是在某些函数调用的返回结果上会存在差异。
  • (2)不同点
  1. HTMLCollection 对象比 NodeList 对象多个 namedItem()函数,可以通过 id 或者 name 属性定位元素。
  2. HTMLCollection 对象只包含元素的集合(Element),即具有标签名的元素;而 NodeList 对象是节点的集合,既包括元素,也包括节点,例如 text 文本节点。

5.3.2 删除节点

firstChild 属性实际是取 childNodes 属性返回的 NodeList 对象中的第一个值,在此例中实际为一个换行符。 如果需要获取第一个元素节点,应该使用 firstElementChild 属性。

在获取文本节点时,需要使用的是 childNodes 属性,然后取返回的 NodeList 对象的第一个值。

在删除文本节点时,我们更推荐使用设置 innerHTML 属性为空的方法。

5.3.3 修改节点

replaceChild()函数的调用方是父元素,接收两个参数,第一个参数表示新元素,第二个参数表示将要被替换的旧元素。

修改属性节点有两种处理方式:一种是通过 getAttribute()函数和 setAttribute()函数获取和设置属性节点值;另一种是直接修改属性名。

5.4 事件流

代表鼠标指针悬浮的 mouseover 事件

一个完整的事件流实际包含了 3 个阶段:事件捕获阶段>事件目标阶段>事件冒泡阶段。

使用 addEventListener()函数绑定的事件在默认情况下,即第三个参数默认为 false 时,按照冒泡型事件流处理。

如果有元素绑定了捕获类型事件,则会优先于冒泡类型事件而先执行。

5.5 事件处理程序

根据 W3C DOM 标准,事件处理程序分为 DOM0、DOM2、DOM3 这 3 种级别的事件处理程序。由于在 DOM1 中并没有定义事件的相关内容,因此没有所谓的 DOM1 级事件处理程序。

5.5.1 DOM0 级事件处理程序

DOM0 级事件处理程序是将一个函数赋值给一个事件处理属性,有两种表现形式。

以上两种 DOM0 级事件处理程序同时存在时,第一种在 JavaScript 中定义的事件处理程序会覆盖掉后面在 html 标签中定义的事件处理程序。

需要注意的是,DOM0 级事件处理程序只支持事件冒泡阶段。

· 缺点:一个事件处理程序只能绑定一个函数。

5.5.2 DOM2 级事件处理程序

DOM2 级事件处理方式规定了添加事件处理程序和删除事件处理程序的方法。

支持对同一个事件绑定多个处理函数。

在需要删除绑定的事件时,不能删除匿名函数,因为添加和删除的必须是同一个函数

在 IE 浏览器中,使用 attachEvent()函数为同一个事件添加多个事件处理函数时,会按照添加的相反顺序执行。

在 IE 浏览器下,使用 attachEvent()函数添加的事件处理程序会在全局作用域中运行,因此 this 指向全局作用域 window。在非 IE 浏览器下,使用 addEventListener()函数添加的事件处理程序在指定的元素内部执行,因此 this 指向绑定的元素。

5.5.3 DOM3 级事件处理程序

最重要的区别在于 DOM3 级事件处理程序允许自定义事件,

5.6 Event 对象

事件在浏览器中是以 Event 对象的形式存在的,每触发一个事件,就会产生一个 Event 对象。该对象包含所有与事件相关的信息,包括事件的元素、事件的类型及其他与特定事件相关的信息。

5.6.1 获取 Event 对象

Event 对象会作为参数传入,参数名为 event。

通过 window.event 属性获取 Event 对象。

5.6.2 获取事件的目标元素

在 IE 浏览器中,event 对象使用 srcElement 属性来表示事件的目标元素;而在非 IE 浏览器中,event 对象使用 target 属性来表示事件的目标元素,

5.6.3 target 属性与 currentTarget 属性

· target 属性在事件目标阶段,理解为真实操作的目标元素。· currentTarget 属性在事件捕获、事件目标、事件冒泡这 3 个阶段,理解为当前事件流所处的某个阶段对应的目标元素。

因此只有在事件目标阶段,target 属性和 currentTarget 属性才指向同一个元素。

5.6.4 阻止事件冒泡

event.stopPropagation()函数。那么在单击 button 按钮时,事件就只会在事件目标阶段执行,而不会向上继续冒泡至父元素

两者的区别主要体现在同一事件绑定多个事件处理程序的情况下。· stopPropagation()函数仅会阻止事件冒泡,其他事件处理程序仍然可以调用。· stopImmediatePropagation()函数不仅会阻止冒泡,也会阻止其他事件处理程序的调用。

5.6.5 阻止默认行为

在众多的 HTML 标签中,有一些标签是具有默认行为的,这里简单地列举 3 个。

· a 标签,在单击后默认行为会跳转至 href 属性指定的链接中。· 复选框 checkbox,在单击后默认行为是选中的效果。· 输入框 text,在获取焦点后,键盘输入的值会对应展示到 text 输入框中。

如何编写代码来阻止元素的默认行为呢?很简单,就是通过 event.preventDefault()函数去实现

Event 对象提供了多种不同的属性来获取键的 Unicode 编码,分别是 event.keyCode、event.charCode 和 event.which。

5.7 事件委托

事件委托是利用事件冒泡原理,管理某一类型的所有事件,利用父元素来代表子元素的某一类型事件的处理方式。

5.7.1 已有元素的事件绑定

事件处理程序过多导致页面交互时间过长。

事件处理程序过多导致内存占用过多。

使用事件委托可以同样很好地解决了不同元素不同处理的情况,从而证明事件委托对于元素事件的处理,尤其是处理多个元素时具有天然优势。

5.7.2 新创建元素的事件绑定

querySelectorAll()函数获取到的 li 元素虽然会实时感知到数量的变化,但并不会实时增加对事件的绑定。

使用事件委托机制,我们可以更加方便快捷地实现新创建元素的事件绑定。

5.8 contextmenu 右键事件

常用的事件类型

· 焦点相关的 focus、blur 等事件。 · 单击相关的 click、dblclick、contextmenu 等事件。 · 鼠标相关的 mouseover、mouseout、mouseenter 等事件。 · 键盘相关的 keydown、keypress、keyup 事件。 · 拖曳相关的 drag 事件。 · 移动端 touch 事件。

5.9.1 load 事件

load 事件会在页面、脚本或者图片加载完成后触发。其中,支持 onload 事件的标签有 body、frame、frameset、iframe、img、link、script。

第一种方式是在 body 标签上使用 onload 属性

第二种方式是设置 window 对象的 onload 属性,属性值为一个函数。

load 事件会在页面中的所有元素全部加载完成后才会去调用

window.load()函数只能绑定一个事件处理程序。

body 标签中使用 onload 属性可以达到

5.9.3 加载完成事件的执行顺序

写在 body 标签中的 onload 属性优先级会高于 window.onload 属性。

5.10.1 重排

浏览器渲染页面默认采用的是流式布局模型。

DOM 结构的修改会决定周边 DOM 结构的更改范围,主要分为全局范围和局部范围。全局范围就是从页面的根节点 html 标签开始,对整个渲染树进行重新计算。例如,当我们改变窗口的尺寸或者修改了根元素的字体大小时,局部范围只会对渲染树的某部分进行重新计算。例如要改变页面中某个 div 的宽度,只需要重新计算渲染树中与该 div 相关的部分即可。

重排的过程就发生在 DOM 节点信息修改的时候,重排实际是根据渲染树中每个渲染对象的信息,计算出各自渲染对象的几何信息,例如 DOM 元素的位置、尺寸、大小等,然后将其安置在界面中正确的位置。

页面首次渲染

浏览器窗口大小发生改变

元素尺寸或位置发生改变

元素内容发生变化

元素字体发生变化

添加或删除可见的 DOM 元素

获取某些特定的属性

浏览器不会针对每个 JS 操作都进行一次重排,而是维护一个会引起重排操作的队列,等队列中的操作达到了一定的数量或者到了一定的时间间隔时,浏览器才会去 flush 一次队列,进行真正的重排操作。

但我们写的一些代码可能会强制浏览器提前 flush 队列,

getComputedStyle():获取元素的 CSS 样式的函数

5.10.2 重绘

重绘只是改变元素在页面中的展现样式,而不会引起元素在文档流中位置的改变

重排一定会引起重绘的操作,而重绘却不一定会引起重排的操作

5.10.3 性能优化

只在最后一步修改 class 类,从而只引起一次重排与重绘的操作

设置其 position 为 absolute 或者 fixed。

,在获取一些特定属性时

应该通过一个变量去缓存,而不是每次都直接获取特定的属性。

如果不得已使用了 table,可以设置 table-layout:auto 或者是 table-layout:fixed。这样可以让 table 一行一行地渲染,这种做法也是为了限制重排的影响范围。

使用事件委托可以在很大程度上减少事件处理程序的数量,从而提高性能

DocumentFragment 最核心的知识点在于它不是真实 DOM 树的一部分,它的变化不会引起 DOM 树重新渲染的操作,也就不会引起浏览器重排和重绘的操作

6.1 Ajax 的基本原理及执行过程

Ajax 的基本原理是通过 XMLHttpRequest 对象向服务器发送异步请求,获取服务器返回的数据后,利用 DOM 的操作来更新页面。

其中最核心的部分就是 XMLHttpRequest 对象。它是一个 JavaScript 对象,支持异步请求,可以及时向服务器发送请求和处理响应,并且不阻塞用户,达到不刷新页面的效果。

6.1.1 XMLHttpRequest 对象

XMLHttpRequest 对象从创建到销毁存在一个完整的生命周期,在生命周期的每个阶段会调用 XMLHttpRequest 对象的不同函数,在函数中需要通过 XMLHttpRequest 对象的特定属性来判断函数执行情况。

readyState

6.1.2 XMLHttpRequest 对象生命周期

每次 readyState 的取值变化时,属性 onreadystatechange 对应的函数都会被执行一次。

当 readyState 的值为 4 时代表响应接收完成,需要注意的是响应接收完成并不代表请求是成功的

因此在 onreadystatechange()回调函数中,我们需要同时判断 readyState 和 status 两个值才能对响应值做正确的处理。

6.1.3 Ajax 的优缺点

该按钮却没有办法和 JavaScript 进行很好的合作,从而导致 Ajax 对浏览器后退机制的破坏。

Ajax 也难以避免一些已知的安全性弱点,例如跨域脚本攻击、SQL 注入攻击和基于 Credentials 的安全漏洞等。

对搜索引擎支持较弱

违背 URL 唯一资源定位的初衷

Ajax 请求并不会改变浏览器地址栏的 URL,因此对于相同的 URL,不同的用户看到的内容可能是不一样的,这就违背了 URL 定位唯一资源的初衷。

6.2 使用 Nodejs 搭建简易服务器

var express = require("express"); // 接收 post 请求体数据的插件
var bodyParser = require("body-parser");
var app = express();
app.use(bodyParser()); // 接收“/”请求,指定首页
app.get("/", function (req, res) {
  res.sendFile(__dirname + "/index.html");
}); // 处理 get 请求
app.get("/getUser", function (req, res) {
  console.log(req.query);
}); // 处理 post 请求
app.post("/saveUser", function (req, res) {
  var responseObj = { code: 200, message: "执行成功" };
  res.write(JSON.stringify(responseObj));
  res.end("end");
}); // 执行监听的端口号
var server = app.listen(3000, function () {});

6.3 使用 Ajax 提交 form 表单

使用 Ajax 提交 form 表单是一种很好的解决办法。因为 Ajax 可以在不刷新页面的情况下提交请求,然后在处理响应时通过 JavaScript 操作 DOM,并展示后台处理的信息。

6.3.2 使用原生 Ajax 进行提交

meta value 有哪些?

Content-type

在设置请求头之前,需要调用 XMLHttpRequest 实例的 open()函数,以保证已经建立连接请求,

6.3.4 使用 jQuery 序列化 form 表单进行提交

使用序列化存在一个很大的缺陷,它并不能处理文件流的数据,只能处理普通的文本数据。

6.3.5 使用 FormData 对象进行提交

FormData 对象是 HTML5 中新增的对象,目前主流的浏览器都已经支持,它的诞生主要是服务于 Ajax 请求,用于发送数据。

FormData 对象提交的最大的优势是可以异步上传文件

application / x - www - form - urlencoded;

var formData = new FormData();

var resume = document.getElementById("resume");

formData.append("resume", resume.files[0]);

xhr.send(formData);

// 第 3 步可以做如下简化。
var formData = new FormData(document.getElementById("userForm"));

当 FormData 对象中包含了 file 文件流的数据时,需要设置 contentType 参数值为默认值 application/x-www-form-urlencoded,而不能设置为 application/json,因为文件流二进制数据不能直接转换为 json 格式。

6.4.1 get 方式和 post 方式的区别

参数传递。

服务端参数获取。

传递的数据量。

安全性。

处理 form 表单的差异性。

因为 form 表单采用 get 请求时,action 指定的 url 中的请求参数会被丢弃

将 form 表单的 method 属性值改为 post,

可以发现 form 表单内的元素和请求 url 中携带的参数都被服务端接收到了,其中 form 表单内的元素通过 Request.body 请求体被服务端接收到,而 url 中携带的参数通过 Request.query 被服务端接收到。

6.4.2 使用 get 方式和 post 方式需要注意的点

get

请求的 url 不发生改变,可能会存在缓存的问题,

编码格式不一致,

导致乱码

xhr.open('get', '/getUser?username='+encodeURIComponent(username), true)

post 方式请求时,需要设置请求头中的 content-type 属性,表示数据在发送至服务器时的编码类型。默认情况下,使用 post 方式提交 form 表单时,content-type 值为 application/x-www-form-unlencoded,另外还可以支持 multipart/formdata、application/json 等格式。

6.5 Ajax 进度事件

Progress Events 规范中增加了 7 个进度事件,

· loadstart:在开始接收响应时触发。· progress:在接收响应期间不断触发,直至请求完成。· error:在请求失败时触发。· abort:在主动调用 abort()函数时触发,表示请求终止。· load:在数据接收完成时触发。· loadend:在通信完成或者 error、abort、load 事件后触发。· timeout:在请求超时时触发。

6.5.1 load 事件

load 事件的诞生是用以代替 readystatechange 事件的

6.5.2 progress 事件

progress 事件会在浏览器接收数据的过程中周期性调用。

,执行完 progress 事件后会依次执行 load、loadend 事件。

6.6 JSON 序列化和反序列化

,JSON 已经变成一种非常流行的数据传输方式。

6.6.1 JSON 序列化

JSON.stringify(value[, replacer [, space]])

在 JSON 序列化时,如果属性值为对象或者数组,则会继续序列化该属性值,直到属性值为基本类型、函数或者 Symbol 类型才结束。

JSON.stringify([new Number(1), new String("false"), new Boolean(false)]);
// '[1,"false",false]'
JSON.stringify({ x: undefined, y: Object, z: Symbol("") }); //'{}'
JSON.stringify([undefined, Object, Symbol("")]);
// '[null,null,null]'

对包含循环引用对象进行序列化时会抛出异常。

不可枚举的属性值会被忽略。

· 如果待序列化的对象存在 toJSON()

函数,则优先调用 toJSON()函数,以 toJSON()函数的返回值作为待序列化的值,否则返回 JSON 对象本身。· 如果 stringify()函数提供了第二个参数 replacer,则对上一步的返回值经过 replacer 参数处理。· 如果 stringify()函数提供了第三个参数,则对 JSON 字符串进行格式化处理,返回最终的结果。

6.6.2 JSON 反序列化

JSON.parse(text[, reviver])

JSON.parse("[1, 2, 3, 4, ]"); // 解析异常,数组最后一个元素后面出现逗号
JSON.parse('{"foo" : 1, }'); // 解析异常,最后一个属性值后面出现逗号

6.7 Ajax 跨域解决方案

什么是浏览器同源策略,浏览器为什么需要跨域限制

6.7.1 浏览器同源策略

同源策略是浏览器最基本也是最核心的安全功能

两个资源路径在协议、域名、端口号上有任何一点不同,则它们就不属于同源的资源。

· DOM 同源策略。

· XMLHttpRequest 同源策略。

6.7.3 Ajax 跨域请求场景

不进行跨域处理会有什么情况

6.7.4 CORS

客户端不能发送跨域请求是因为服务端并不接收跨域的请求,

CORS 解决办法,主要实现方式是服务端通过对响应头的设置,接收跨域请求处理。

// 设置可以接收请求的域名 res.header('Access-Control-Allow-Origin', 'http://localhost:4000');

通过服务端的处理不会对前端代码做任何修改,但是由于服务端采用的语言、框架多变,处理方式会依赖各种语言的特性。

6.7.5 JSONP

JSONP 是客户端与服务器端跨域通信最常用的解决办法,它的特点是简单适用、兼容老式浏览器、对服务器端影响小。

动态添加一个 script 标签,通过 script 标签向服务器发送请求,在请求中会携带一个请求的 callback 回调函数名。

处理响应获取返回的参数,

并将 callback 回调函数通过 json 格式进行返回。

回调函数必须设置为全局函数。

// 返回值是对回调函数的调用,将 data 作为参数传入
res.write(callbackFn + "(" + data + ")");

只支持 get 请求,这是 JSONP 目前最大的缺点。如果是 post 请求,那么 JSONP 则无法完成跨域处理。

响应依赖于其他域的实现,如果请求的其他域不安全,可能会对本域造成一定的安全性影响。

很难确定 JSONP 请求是否失败,虽然在 HTML5 中给 script 标签增加了 onerror 事件处理程序,但是存在兼容性问题。

7.1.1 let 关键字

暂时性死区

不能重复声明

不再是全局对象的属性

不会导致 for 循环索引值泄露

代替立即执行函数 IIFE

7.2.1 数组的解构赋值

交换变量

7.2.2 对象的解构赋值

let { min, max } = Math;
console.log(min(1, 3)); // 1
console.log(max(1, 3)); // 3

7.3 扩展运算符与 rest 运算符

这两种运算符可以很好地解决函数参数和数组元素长度未知情况下的编码问题,使得代码能更加健壮和简洁。

7.3.1 扩展运算符

扩展运算符可以代替 apply()函数,将数组转换为函数参数。

let arr = [1, 2, 4, 6, 2, 7, 4];
console.log([...new Set(arr)]); // [ 1, 2, 4, 6, 7 ]

使用扩展运算符对数组或对象进行克隆时,如果数组的元素或者对象的属性是基本数据类型,则支持深克隆;如果是引用数据类型,则不支持深克隆

7.3.2 rest 运算符

其作用与扩展运算符相反,用于将以逗号分隔的值序列转换成数组。

当 3 个点(…)出现在函数的形参上或者出现在赋值等号的左侧,则表示它为 rest 运算符。

当 3 个点(…)出现在函数的实参上或者出现在赋值等号的右侧,则表示它为扩展运算符。

7.5.1 箭头函数的特点

从严格意义上讲,箭头函数中不会创建自己的 this,而是会从自己作用域链的上一层继承 this。

使用 call()函数和 apply()函数调用箭头函数时,需要谨慎。

箭头函数中使用 arguments 时,会抛出异常。

const fn = (...args) => { console.log(args); };fn(1, 2); // [1, 2]

7.5.2 箭头函数不适用的场景

// 箭头函数
let Person = (name) => {
  this.name = name;
};
let p = new Person("kingx"); // Uncaught TypeError: Person is not a constructor

let a = () => {
  return 1;
};
function b() {
  return 2;
}
console.log(a.prototype); // undefined
console.log(b.prototype); // {constructor: ƒ}

7.6.1 属性简写

属性简写

7.6.2 属性遍历

有 5 种方法可以实现对象属性的遍历,

  1. for...in。
  2. Object.keys(obj)。
  3. Object.getOwnPropertyNames(obj)。
  4. Object.getOwnPropertySymbols(obj)。
  5. Reflect.ownKeys(obj)。

Object.getOwnPropertySymbols()函数返回一个数组,包含对象自身所有 Symbol 属性,不包含其他属性。

Reflect.ownKeys()函数返回一个数组,包含可枚举属性、不可枚举属性以及 Symbol 属性,不包含继承属性。

7.6.3 新增 Object.assign()函数

Object.assign()函数进行克隆时,进行的是浅克隆。

// 传统的写法
function Person(name, age, address) {
  this.name = name;
  this.age = age;
  this.address = address;
} // Object.assign()写法
function Person(name, age, address) {
  Object.assign(this, { name, age, address });
}

// 传统写法
Person.prototype.getName = function () {
  return this.name;
};
Person.prototype.getAge = function () {
  return this.age;
}; // Object.assign()写法
Object.assign(Person.prototype, {
  getName() {
    return this.name;
  },
  getAge() {
    return this.age;
  },
});

// 多个对象合并到一个目标对象中
const merge = (target, ...sources) => Object.assign(target, ...sources); // 多个对象合并为一个新对象并返回
const merge = (...sources) => Object.assign({}, ...sources);

7.7.1 Symbol 类型的特性

let s1 = new Symbol(); // TypeError: Symbol is not a constructor

let s4 = Symbol("hello");
s4.toString(); // Symbol(hello)'s4 content is: ' + s4; // TypeError: Cannot convert a Symbol value to a string

let s1 = Symbol.for("one");
let s2 = Symbol.for("one");
s1 === s2; // true

Symbol.for()函数与 Symbol()函数这两种写法,都会生成新的 Symbol 值。它们的区别是,前者会被登记在全局环境中以供搜索,而后者不会。

7.7.2 Symbol 类型的用法

不能通过点运算符为对象添加 Symbol 属性。

可以将一些不需要对外操作和访问的属性通过 Symbol 来定义。

Symbol 属性不会出现在属性遍历的过程中,所以在使用 JSON.stringify()函数将对象转换为 JSON 字符串时,Symbol 值也不会出现在结果中。

// 使用 Object 的 API
Object.getOwnPropertySymbols(obj); // [Symbol(name)]
// 使用新增的反射 API
Reflect.ownKeys(obj); // [Symbol(name), 'age', 'title']

7.8 Set 数据结构和 Map 数据结构

add()函数添加新值时,新值与 Set 实例中原有值是采用严格相等(===)进行比较的

对于 NaN 是一个特例,NaN 与 NaN 在进行严格相等的比较时是不相等的,但是在 Set 内部,NaN 与 NaN 是严格相等的,因此一个 Set 实例中只可以添加一个 NaN。

Set 中没有索引的概念,它实际是键和值相同的集合

除了 forEach()函数外,我们还可以使用以下 3 种函数对 Set 实例进行遍历。· keys():返回键名的遍历器。· values():返回键值的遍历器。· entries():返回键值对的遍历器

7.8.2 Map 数据结构

Map 的键却可以由各种类型的值组成

Map 本身是一个构造函数,可以接收一个数组作为参数,数组的每个元素同样是一个子数组,子数组元素表示的是键和值。

所有的键都必须具有唯一性。

forEach()函数、keys()函数、values()函数、entries()函数。

//Map 转换为数组
const map = new Map();
map.set("name", "kingx");
map.set("age", 12);
const arr = [...map];
console.log(arr); // [ [ 'name', 'kingx' ], [ 'age', 12 ] ]

//Map 转换为对象
const arr = [
  ["name", "kingx"],
  ["age", 12],
];
const map = new Map(arr);
console.log(map); // Map { 'name' => 'kingx', 'age' => 12 }

7.9.2 Proxy 实例函数及其基本使用

get()函数只是 Proxy 实例支持的总共 13 种函数中的一种

不可写且不可配置的属性只能返回其实际值。

has()函数只会对 in 操作符生效,而不会对 for...in 循环操作符生效。

7.10 Reflect

有一个名为 Reflect 的全局对象,上面挂载了对象的某些特殊函数,

Reflect 对象上的函数要么可以在 Object 原型链中找到,要么可以通过命令式操作符实现,例如 delete 和 in 操作符。

更合理地规划与 Object 对象相关的 API。

将一些命令式的操作符如 delete、in 等使用函数来替代,这样做的目的是为了让代码更好维护,更容易向下兼容,同时也避免出现更多的保留字。

// 传统写法
"assign" in Object; // true
// 新写法
Reflect.has(Object, "assign"); // true

事实上 Proxy 对象也会经常随着 Reflect 对象一起进行调用,

7.10.2 Reflect 静态函数

与 Proxy 对象不同的是,Reflect 对象本身并不是一个构造函数,而是直接提供静态函数以供调用,Reflect 对象的静态函数一共有 13 个

7.10.3 Reflect 与 Proxy

Reflect 对象的函数与 Proxy 对象的函数一一对应,因此在 Proxy 对象中调用 Reflect 对象对应的函数是一个明智的选择。

最经典的案例就是可以实现观察者模式。

7.11.2 Promise 的生命周期

每一个 Promise 对象都有 3 种状态,即 pending(进行中)、fulfilled(已成功)和 rejected(已失败)。

7.11.3 Promise 的基本用法

// 封装原生 get 类型 Ajax 请求
function ajaxGetPromise(url) {
  const promise = new Promise(function (resolve, reject) {
    const handler = function () {
      if (this.readyState !== 4) {
        return;
      }
      // 当状态码为 200 时,表示请求成功,执行 resolve()函数
      if (this.status === 200) {
        // 将请求的响应体作为参数,传递给 resolve()函数
        resolve(this.response);
      } else {
        // 当状态码不为 200 时,表示请求失败,reject()函数
        reject(new Error(this.statusText));
      }
    }; // 原生 Ajax 请求操作
    const client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.responseType = "json";
    client.setRequestHeader("Accept", "application/json");
    client.send();
  });
  return promise;
}

· 只有 p1、p2、p3 全部的状态都变为 fulfilled 成功状态,p 的状态才会变为 fulfilled 状态,此时 p1、p2、p3 的返回值组成一个数组,作为 p 的 then()函数的回调函数的参数。

· 只要 p1、p2、p3 中有任意一个状态变为 rejected 失败状态,p 的状态就变为 rejected 状态,此时第一个被 reject 的实例的返回值会作为 p 的 catch()函数的回调函数的参数。

p2 实例执行完 catch()函数后,p2 的状态实际是变为 fulfilled,只不过它的返回值是 Error 的信息。

使用 Promise.race()函数可以实现这样一个场景:假如发送一个 Ajax 请求,在 3 秒后还没有收到请求成功的响应时,会自动处理成请求失败。

7.11.4 Promise 的用法实例

在状态变换完成后,如果成功会触发所有的 then()函数,如果失败会触发所有的 catch()函数。

不同于使用 throw 抛出一个 Error,如果是 throw 抛出一个 Error 则会被 catch()函数捕获。

在 Promise 的 then()函数或者 catch()函数中,接收的是一个函数

如果传入的值是非函数,那么就会产生值穿透现象。

Promise.resolve()
  .then(
    function success(res) {
      throw new Error("error");
    },
    function fail1(e) {
      console.error("fail1: ", e);
    }
  )
  .catch(function fail2(e) {
    console.error("fail2: ", e);
  });

而 catch()函数却能捕获到第一个函数中抛出的异常。

7.12 Iterator 与 for...of 循环

一个合法的 Iterator 接口都会具有一个 next()函数,在遍历的过程中,依次调用 next()函数,返回一个带有 value 和 done 属性的对象。value 值表示当前遍历到的值,done 值表示迭代是否结束,

7.12.2 默认 Iterator 接口

· Array。· Map。

· Set。· String。· 函数的 arguments 对象。· NodeList 对象。

7.13 Generator()函数

Generator()函数从语法上可以理解为是一个状态机,函数内部维护多个状态,函数执行的结果返回一个部署了 Iterator 接口的对象,通过这个对象可以依次获取 Generator()函数内部的每一个状态。

function 关键字与函数名之间有一个星号*。· 函数体内部使用 yield 关键字来定义不同的内部状态。

function* helloworldGenerator() {
  yield "hello";
  yield "world";
  return "success";
}
const hw = helloworldGenerator();
hw.next(); // {value: "hello", done: false}
hw.next(); // {value: "world", done: false}
hw.next(); // {value: "success", done: true}

yield 语句本身没有返回值,

7.13.2 Generator()函数注意事项

Generator()函数并不是构造函数

不能使用 new 关键字生成 Generator 的实例,因此 Generator()函数中的 this 是无效的。

7.14 Class

class 的本质还是一个函数,只不过是函数的另一种写法,这种写法可以让对象的原型属性和函数更加清晰。

事实上,class 中的所有属性和函数都是定义在 prototype 属性中的

constructor()函数默认会返回当前对象的实例,即默认的 this 指向,我们可以手动修改返回值

使用 static 关键字修饰时,静态属性和函数无法被实例访问,只能通过类自身使用。

静态函数中的 this 指向的是类本身,而不是类的实例,

因为静态函数和实例函数中的 this 是隔离的,所以同一个类中可以存在函数名相同的静态函数和实例函数。

class 定义的类只能配合 new 关键字生成实例,不能像普通函数一样直接调用。

在定义类之前去使用它,会抛出引用异常

不要加 function 关键字

ES6 的 class 关键字中使用了严格模式

7.14.2 class 继承

extends 关键字不仅可以继承自定义的类,还可以继承原生的内置构造函数。

父类的静态函数无法被实例继承,但可以被子类继承。子类在访问时同样是通过本身去访问,而不是通过子类实例去访问。

7.15 Module

CommonJS 在运行时完成模块的加载,而 ES6 模块是在编译时完成模块的加载,效率要更高。· CommonJS 模块是对象,而 ES6 模块可以是任何数据类型,通过 export 命令指定输出的内容,并通过 import 命令引入即可。· CommonJS 模块会在 require 加载时完成执行,而 ES6 的模块是动态引用,只在执行时获取模块中的值。

7.15.2 export 命令

不能直接通过 export 输出变量值,而是需要对外提供接口,必须与模块内部的变量建立一一对应的关系,

let obj = {};
function foo() {}
export let a = 1; // 正确写法
export { obj }; // 正确写法
export { foo }; // 正确写法

export 对外输出的接口,在外部模块引用时,是实时获取的,并不是 import 那个时刻的值

可以使用 as 关键字设置别名,同一个属性可以设置多个别名。

同一个变量名只能够 export 一次,

7.15.3 import 命令

在 HTML 页面中使用 import 命令,需要在 script 标签上使用代码 type="module"

import 命令引入的变量需要放在一个大括号里,括成对象的形式,而且 import 的变量名必须与 export 的变量名一致。

相同变量名的值只能 import 一次,

从多个不同的模块中 import 进相同的变量名,则会抛出异常

import 命令具有提升的效果,会将 import 的内容提升到文件头部

本质上是因为 import 是在编译期运行的,在执行输出代码之前已经执行了 import 语句。

每个模块只加载一次,每个 JS 文件只执行一次,如果在同一个文件中多次 import 相同的模块,则只会执行一次模块文件,后续直接从内存读取。

// export.js
console.log("开始执行");
export const name = "kingx";
export const age = 12;
// import.js
import { name } from "./export.js";
import { age } from "./export.js";

在上面的代码中,import 两次 export.js 文件,但是最终只输出了一次“开始执行”,可以理解为 import 导入的模块是个单例模式。

使用 import 命令导入的值,相当于一个 const 常量;

同样可以使用 as 关键字为变量设置别名,

需要加载整个模块的内容时,可以使用星号*配合 as 关键字指定一个对象,通过对象去访问各个输出值。

使用了星号,就不能再使用大括号{}括起来

7.15.4 export default 命令

只能有一个 export default 语句,代表一个唯一的默认输出

使用 import 命令引入默认的变量时,不需要使用大括号括起来

7.15.5 Module 加载的实质

ES6 模块的运行机制是这样的:当遇到 import 命令时,不会立马去执行模块,而是生成一个动态的模块只读引用,等到需要用到时,才去解析引用对应的值。

导入的值仍然与原来的模块存在引用关系,并不是完全隔断的。

扩展

Proxy 能监听什么open in new window