本文主要记录了 JS 基础以及 ES6 的相关笔记,随缘更新。

JS 基础

基本概念

  1. JS 中 label 标签用来终止或者跳过外层循环

    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
    top: for (var i = 0; i < 3; i++) {
      for (var j = 0; j < 3; j++) {
        if (i === 1 && j === 1) break top;
        console.log("i=" + i + ", j=" + j);
      }
    }
    // i=0, j=0
    // i=0, j=1
    // i=0, j=2
    // i=1, j=0
    
    // 直接进入下一轮外层循环
    top: for (var i = 0; i < 3; i++) {
      for (var j = 0; j < 3; j++) {
        if (i === 1 && j === 1) continue top;
        console.log("i=" + i + ", j=" + j);
      }
    }
    // i=0, j=0
    // i=0, j=1
    // i=0, j=2
    // i=1, j=0
    // i=2, j=0
    // i=2, j=1
    // i=2, j=2
  2. 在 ES5 之前,JS 有六种基本对象

    • number
    • string
    • boolean
    • object
    • undefined
    • null
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    typeof window; // "object"
    typeof {}; // "object"
    typeof []; // "object"
    function f() {}
    typeof f;
    // "function"
    v;
    // ReferenceError: v is not defined
    typeof NaN; // 'number'
    typeof v;
    // "undefined"
    typeof null; // "object"

    typeof null的结果是object,这是由于 JS 历史遗留问题造成的。

  3. JS 中所有数字都是以 64 位浮点数表示的,而有些小数用二进制不能精确的表示,例如

    要理解这个问题,我们需要知道十进制和二进制的区别。十进制是以 10 为基数的数制,也就是说,每个位上的数字都是 10 的幂次方的系数。比如,123.45 可以表示为:

    二进制是以 2 为基数的数制,也就是说,每个位上的数字都是 2 的幂次方的系数。比如,101.11 可以表示为:

    当我们把一个十进制小数转换成二进制小数时,我们需要不断地用 2 去 x 这个小数的小数部分,并把余数作为二进制位。比如,0.1 转换成二进制的过程如下:

    可以看到,当我们遇到了之前出现过的余数 0.2 时,就会出现循环。所以,0.1 的二进制表示就是:

    其中$\overline{0011}$表示 0011 这个部分是无限重复的。

    如果一个十进制小数能被 2 整除,那么它就能用有限的二进制位来表示。比如,0.5 可以表示为$(0.1)_2$,因为$0.5 \div 2 = 0 … 1$。但如果一个十进制小数不能被 2 整除,那么它就可能用无限的二进制位来表示。比如,0.3 可以表示为$(0.\overline{010011})_2$³。

  4. 数值转换函数。

    • parseInt:将一个字符串转化为整数。
    • parseFloat:将一个字符串转化为浮点数。
    • isFinite:判断是否是正常的数值。先转化成数值再进行判断。
    • isNaN:判断是否是NaN。先转换成数值再进行判断。
  5. JS 在圆括号里面只能是表达式,在遇到花括号的时候一律解释为代码块。

  6. delete用于删除对象的属性,但有注意点,一是不能通过delete语句的结果来判断删除属性是否成功,因为删除一个不存在的属性delete也会返回true,只有当对象属性设置为不可配置的时候返回false。而且只能删除自身的属性,不能删除继承的属性。

    1
    2
    3
    4
    5
    6
    var obj = { p: 1 };
    Object.keys(obj); // ["p"]
    
    delete obj.p; // true
    obj.p; // undefined
    Object.keys(obj); // []
  7. in运算符可以判断对象属性是否含有某个key,注意会沿着对象的原型链进行查找。

    1
    2
    3
    4
    5
    6
    7
    8
    var person = { name: "老张" };
    
    for (var key in person) {
      if (person.hasOwnProperty(key)) {
        console.log(key);
      }
    }
    // name

    你可以传递任意数量的参数给Function构造函数,只有最后一个参数会被当做函数体,如果只有一个参数,该参数就是函数体。

    1
    2
    3
    4
    5
    6
    var foo = new Function('return "hello world";');
    
    // 等同于
    function foo() {
      return "hello world";
    }
  8. var提升只会把声明提升,不会提升赋值语句。

    1
    2
    3
    4
    5
    6
    7
    f();
    var f = function () {};
    // TypeError: undefined is not a function
    // 等同于
    var f;
    f();
    f = function () {};
  9. 函数的name属性会显示函数的名字,函数的length会显示函数预期传入的参数的个数。这个属性可用于函数的方法的重载。

    为何内置函数的toString方法返回的是function (){[native code]},这是因为JS内部方法的函数不是通过原生JS实现的,而是由C++等代码实现的。

  10. 闭包的最大用处有两个,一个是可以读取外层函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。闭包的另一个用处,是封装对象的私有属性和私有方法。
  11. 为了避免解析的歧义,JavaScript规定,如果function关键字出现在行首,一律解释成语句。因此,引擎看到行首是function关键字之后,认为这一段都是函数的定义,不应该以圆括号结尾,所以就报错了。

    1
    2
    3
    4
    5
    6
    7
    8
    function(){ /* code */ }();
    // SyntaxError: Unexpected token (
      var f = function f(){ return 1}();
    f // 1
    
    (function(){ /* code */ }());
    // 或者
    (function(){ /* code */ })();

对象 Object

  1. 数组的length是一个动态属性,只要是数组,就一定有length属性。该属性是一个动态的值,等于键名中的最大整数加上1,没有显示的元素填空值。更改length属性会导致,数组长度增大,添加空值。或减小,直接裁剪元素。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var arr = ["a", "b"];
    arr.length; // 2
    
    arr[2] = "c";
    arr.length; // 3
    
    arr[9] = "d";
    arr.length; // 10
    
    arr[1000] = "e";
    arr.length; // 1001
  2. 注意一些方法对于数组空位的处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    var a = [, , ,];
    
    a.forEach(function (x, i) {
      console.log(i + ". " + x);
    });
    // 不产生任何输出
    
    for (var i in a) {
      console.log(i);
    }
    // 不产生任何输出
    
    Object.keys(a);
    // []
    
    var a = [undefined, undefined, undefined];
    
    a.forEach(function (x, i) {
      console.log(i + ". " + x);
    });
    // 0. undefined
    // 1. undefined
    // 2. undefined
    
    for (var i in a) {
      console.log(i);
    }
    // 0
    // 1
    // 2
    
    Object.keys(a);
    // ['0', '1', '2']
  3. Object.keys()Object.getOwnPropertyNames二者返回结果在大多数情况下都是相同的,但前者只会返回可枚举的属性,后者可以返回不可枚举的属性。二者都不能获取原型链上面的属性。

    1
    2
    3
    4
    var a = ["Hello", "World"];
    
    Object.keys(a); // ["0", "1"]
    Object.getOwnPropertyNames(a); // ["0", "1", "length"]
  4. valueOf 方法的主要用途是,JavaScript 自动类型转换时会默认调用这个方法。

    1
    2
    3
    4
    5
    6
    var obj = new Object();
    obj.valueOf = function () {
      return 2;
    };
    
    1 + obj; // 3
  5. Object.defineProperty方法的详细解释
    Object.defineProperty 方法是一个 JavaScript 的静态方法,它可以用来在一个对象上直接定义一个新属性,或者修改一个已经存在的属性,并返回该对象。这个方法的作用是可以精确地控制对象属性的特征,比如是否可写、是否可枚举、是否可配置等。Object.defineProperty 方法的语法如下:

    1
    Object.defineProperty(obj, prop, descriptor);

    其中,obj 是要定义或修改属性的对象,prop 是要定义或修改的属性名,descriptor 是一个对象,用来描述该属性的特征。descriptor 对象可以包含以下几个键:

    • value:该属性的值,可以是任何有效的 JavaScript 值,默认为 undefined
    • writable:该属性是否可写,如果为 true,则可以通过赋值运算符修改该属性的值,否则不能,默认为 false
    • enumerable:该属性是否可枚举,如果为 true,则可以通过 for...in 循环或 Object.keys 方法遍历该属性,否则不能,默认为 false
    • configurableconfigurable是一个布尔值,表示属性的可配置性,默认为false。如果设为false,将阻止某些操作改写属性描述对象,比如无法删除该属性,也不得改变各种元属性(value属性除外)。也就是说,configurable属性控制了属性描述对象的可写性。
    • get:该属性的 getter 函数,用来返回该属性的值,如果没有 getter 函数,则为 undefined。当访问该属性时,会调用这个函数,并把 this 绑定到所属对象上。默认为 undefined
    • set:该属性的 setter 函数,用来设置该属性的值,如果没有 setter函数,则为undefined。当给该属性赋值时,会调用这个函数,并把this绑定到所属对象上。默认为 undefined

    注意:descriptor 对象中不能同时出现 valuewritablegetset 这两组键,否则会抛出异常。也就是说,一个属性要么是数据描述符(有 valuewritable),要么是访问器描述符(有 getset)。

    使用 Object.defineProperty 方法可以创建或修改对象的属性,并且可以精细地控制其特征。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    // 创建一个空对象
    let obj = {};
    
    // 定义一个普通的数据属性
    Object.defineProperty(obj, "a", {
      value: 1,
      writable: true,
      enumerable: true,
      configurable: true,
    });
    
    // 定义一个只读的数据属性
    Object.defineProperty(obj, "b", {
      value: 2,
      writable: false,
      enumerable: true,
      configurable: true,
    });
    
    // 定义一个不可枚举的数据属性
    Object.defineProperty(obj, "c", {
      value: 3,
      writable: true,
      enumerable: false,
      configurable: true,
    });
    
    // 定义一个不可配置的数据属性
    Object.defineProperty(obj, "d", {
      value: 4,
      writable: true,
      enumerable: true,
      configurable: false,
    });
    
    // 定义一个访问器属性
    let eValue = 5;
    Object.defineProperty(obj, "e", {
      get() {
        return eValue;
      },
      set(newValue) {
        eValue = newValue;
      },
      enumerable: true,
      configurable: true,
    });
    
    console.log(obj.a); // 1
    console.log(obj.b); // 2
    console.log(obj.c); // 3
    console.log(obj.d); // 4
    console.log(obj.e); // 5
    
    obj.a = 10; // 可以修改a的值
    obj.b = 20; // 不可以修改b的值
    obj.e = 50; // 可以通过setter函数修改e的值
    
    console.log(obj.a); // 10
    console.log(obj.b); // 2
    console.log(obj.e); // 50
    
    for (let key in obj) {
      console.log(key); // a b d e (c不可枚举)
    }
    
    delete obj.a; // 可以删除a属性
    delete obj.b; // 可以删除b属性
    delete obj.c; // 可以删除c属性
    delete obj.d; // 不可以删除d属性(不可配置)
    delete obj.e; // 可以删除e属性
    
    console.log(obj.a); // undefined
    console.log(obj.b); // undefined
    console.log(obj.c); // undefined
    console.log(obj.d); // 4
    console.log(obj.e); // undefined
    
    // 修改d的特征(不可配置)
    Object.defineProperty(obj, "d", {
      value: 40,
      writable: false,
      enumerable: false,
      configurable: true, // 报错
    });
    
    // 有关 get 与 set 的设置
    let Person = {};
    let temp = null;
    
    // 定义一个有名字的函数
    function test(val) {
      if (arguments.length == 0) {
        // 如果没有参数,表示是getter
        return temp;
      } else {
        // 如果有参数,表示是setter
        temp = val;
      }
    }
    
    // 把有名字的函数赋给set和get
    Object.defineProperty(Person, "name", {
      get: test, // 不加括号
      set: test, // 不加括号
    });
  6. Object.getOwnPropertyDescriptor()方法可以获取属性描述对象。它的第一个参数是目标对象,第二个参数是一个字符串,对应目标对象的某个属性名。不能用于获取继承属性。

    1
    2
    3
    4
    5
    6
    7
    8
    var obj = { p: "a" };
    
    Object.getOwnPropertyDescriptor(obj, "p");
    // Object { value: "a",
    //   writable: true,
    //   enumerable: true,
    //   configurable: true
    // }
  7. Object.getOwnPropertyNames方法返回一个数组,成员是参数对象自身的全部属性的属性名,不管该属性是否可遍历。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var obj = Object.defineProperties(
      {},
      {
        p1: { value: 1, enumerable: true },
        p2: { value: 2, enumerable: false },
      }
    );
    
    Object.getOwnPropertyNames(obj);
    // ["p1", "p2"]
  8. 实例对象的propertyIsEnumerable()方法返回一个布尔值,用来判断某个属性是否可遍历。注意,这个方法只能用于判断对象自身的属性,对于继承的属性一律返回false

    1
    2
    3
    4
    5
    var obj = {};
    obj.p = 123;
    
    obj.propertyIsEnumerable("p"); // true
    obj.propertyIsEnumerable("toString"); // false
  9. 如果原型对象的某个属性的writablefalse,那么子对象将无法自定义这个属性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    var proto = Object.defineProperty({}, "foo", {
      value: "a",
      writable: false,
    });
    
    var obj = Object.create(proto);
    
    obj.foo = "b";
    obj.foo; // 'a'
    
    // 规避方法
    var proto = Object.defineProperty({}, "foo", {
      value: "a",
      writable: false,
    });
    
    var obj = Object.create(proto);
    Object.defineProperty(obj, "foo", {
      value: "b",
    });
    
    obj.foo; // "b"
  10. 具体来说,如果一个属性的enumerablefalse,下面三个操作不会取到该属性。

    • for..in循环
    • Object.keys方法
    • JSON.stringify方法
  11. 当一个对象的属性的configurable设置为false的时候,writable属性只有在false改为true时会报错,true改为false是允许的。value属性的情况比较特殊。只要writableconfigurable有一个为true,就允许改动value

    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
    var obj = Object.defineProperty({}, "p", {
      writable: true,
      configurable: false,
    });
    
    Object.defineProperty(obj, "p", { writable: false });
    // 修改成功
    
    var o1 = Object.defineProperty({}, "p", {
      value: 1,
      writable: true,
      configurable: false,
    });
    
    Object.defineProperty(o1, "p", { value: 2 });
    // 修改成功
    
    var o2 = Object.defineProperty({}, "p", {
      value: 1,
      writable: false,
      configurable: true,
    });
    
    Object.defineProperty(o2, "p", { value: 2 });
    // 修改成功
  12. getset关键字的使用

    上面两种写法,虽然属性 p 的读取和赋值行为是一样的,但是有一些细微的区别。第一种写法,属性 p 的 configurable 和 enumerable 都为 false,从而导致属性 p 是不可遍历的;第二种写法,属性 p 的 configurable 和 enumerable 都为 true,因此属性 p 是可遍历的。实际开发中,写法二更常用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 写法二
    var obj = {
      get p() {
        return "getter";
      },
      set p(value) {
        console.log("setter: " + value);
      },
    };

内置对象

  1. 利用Object.prototype.toString方法来写一个能够判断所有数据类型的函数

    1
    2
    3
    4
    5
    6
    7
    const getType = (obj) => {
      const type = typeof obj;
      if (type !== "object") return type;
      return Object.prototype.toString
        .call(obj)
        .replace(/^\[object (\S+)\]$/, "$1");
    };
  2. 数组的toString方法,注意数组的shiftpop方法会返回当前弹出的值。数组的join方法,如果数组成员是undefinednull或空位,会被转成空字符串。数组的splice方法也能使用负数索引,只使用一个参数就会把从当前索引往后全部删除掉。大部分遍历类型的数组原生函数都会跟三个对象element, index, arr

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    var a = ["a", "b", "c", "d", "e", "f"];
    a.splice(4, 2, 1, 2); // ["e", "f"]
    a; // ["a", "b", "c", "d", 1, 2]
    var a = [1, 1, 1];
    a.splice(1, 0, 2); // []
    a; // [1, 2, 1, 1]
    var a = [1, 2, 3, 4];
    a.splice(2); // [3, 4]
    a; // [1, 2]
    var arr = [1, 2, 3];
    arr.toString(); // "1,2,3"
    
    var arr = [1, 2, 3, [4, 5, 6]];
    arr.toString(); // "1,2,3,4,5,6"
    var a = [1, 2, 3, 4];
    
    a.join(" "); // '1 2 3 4'
    a.join(" | "); // "1 | 2 | 3 | 4"
    a.join(); // "1,2,3,4"
    
    Array.prototype.join.call("hello", "-");
    // "h-e-l-l-o"
    
    var obj = { 0: "a", 1: "b", length: 2 };
    Array.prototype.join.call(obj, "-");
    // 'a-b'
    
    var a = ["a", "b", "c", "d", "e", "f"];
    a.splice(-4, 2); // ["c", "d"]
    
    function log(element, index, array) {
      console.log("[" + index + "] = " + element);
    }
    
    [2, 5, 9].forEach(log);
    // [0] = 2
    // [1] = 5
    // [2] = 9
  3. 注意,对于空数组,some方法返回falseevery方法返回true,回调函数都不会执行。

  4. toFixed()方法先将一个数转为指定位数的小数,然后返回这个小数对应的字符串。

    由于浮点数的原因,小数 5 的四舍五入是不确定的,使用的时候必须小心。

    1
    2
    3
    4
    5
    (10).toFixed(2); // "10.00"
    (10.005).toFixed(2); // "10.01"
    
    (10.055).toFixed(2); // 10.05
    (10.005).toFixed(2); // 10.01
  5. Number.prototype.toPrecision方法用于将一个数转为指定位数的有效数字。

    由于浮点数的原因,该方法四舍五入依然不可靠。

    1
    2
    3
    4
    5
    (12.34).toPrecision(1); // "1e+1"
    (12.34).toPrecision(2); // "12"
    (12.34).toPrecision(3); // "12.3"
    (12.34).toPrecision(4); // "12.34"
    (12.34).toPrecision(5); // "12.340"
  6. toLocaleString方法的使用。

    1
    2
    3
    4
    5
    6
    7
    8
    (123).toLocaleString("zh-Hans-CN", { style: "currency", currency: "CNY" });
    // "¥123.00"
    
    (123).toLocaleString("de-DE", { style: "currency", currency: "EUR" });
    // "123,00 €"
    
    (123).toLocaleString("en-US", { style: "currency", currency: "USD" });
    // "$123.00"
  7. Math对象的相关方法。

    • Math.abs():绝对值
    • Math.ceil():向上取整
    • Math.floor():向下取整
    • Math.max():最大值
    • Math.min():最小值
    • Math.pow():幂运算
    • Math.sqrt():平方根
    • Math.log():自然对数
    • Math.exp():e 的指数
    • Math.round():四舍五入
    • Math.random():随机数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    function ToInteger(x) {
      x = Number(x);
      return x < 0 ? Math.ceil(x) : Math.floor(x);
    }
    
    ToInteger(3.2); // 3
    ToInteger(3.5); // 3
    ToInteger(3.8); // 3
    ToInteger(-3.2); // -3
    ToInteger(-3.5); // -3
    ToInteger(-3.8); // -3
    
    Math.round(0.1); // 0
    Math.round(0.5); // 1
    Math.round(0.6); // 1
    
    // 等同于
    Math.floor(x + 0.5);
    Math.round(-1.1); // -1
    Math.round(-1.5); // -1
    Math.round(-1.6); // -2
  8. Date对象可以作为普通函数直接调用,返回一个代表当前时间的字符串。Date实例求值默认调用的是toString方法而不是valueOf方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    // 参数为时间零点开始计算的毫秒数
    new Date(1378218728000);
    // Tue Sep 03 2013 22:32:08 GMT+0800 (CST)
    
    // 参数为日期字符串
    new Date("January 6, 2013");
    // Sun Jan 06 2013 00:00:00 GMT+0800 (CST)
    
    // 参数为多个整数,
    // 代表年、月、日、小时、分钟、秒、毫秒
    new Date(2013, 0, 1, 0, 0, 0, 0);
    // Tue Jan 01 2013 00:00:00 GMT+0800 (CST)
    
    new Date(2013);
    // Thu Jan 01 1970 08:00:02 GMT+0800 (CST)
    
    new Date(2013, 0);
    // Tue Jan 01 2013 00:00:00 GMT+0800 (CST)
    new Date(2013, 0, 1);
    // Tue Jan 01 2013 00:00:00 GMT+0800 (CST)
    new Date(2013, 0, 1, 0);
    // Tue Jan 01 2013 00:00:00 GMT+0800 (CST)
    new Date(2013, 0, 1, 0, 0, 0, 0);
    // Tue Jan 01 2013 00:00:00 GMT+0800 (CST)
    
    // 返回时间戳
    Date.now(); // 1364026285194
    
    new Date("2013-2-15");
    new Date("2013/2/15");
    new Date("02/15/2013");
    new Date("2013-FEB-15");
    new Date("FEB, 15, 2013");
    new Date("FEB 15, 2013");
    new Date("February, 15, 2013");
    new Date("February 15, 2013");
    new Date("15 Feb 2013");
    new Date("15, February, 2013");
    // Fri Feb 15 2013 00:00:00 GMT+0800 (CST)
    Date.parse("Aug 9, 1995");
    Date.parse("January 26, 2011 13:51:50");
    Date.parse("Mon, 25 Dec 1995 13:30:00 GMT");
    Date.parse("Mon, 25 Dec 1995 13:30:00 +0430");
    Date.parse("2011-10-10");
    Date.parse("2011-10-10T14:48:00");
  9. JSON的格式规定

    1. 复合类型的值只能是数组或对象,不能是函数、正则表达式对象、日期对象。
    2. 原始类型的值只有四种:字符串、数值(必须以十进制表示)、布尔值和 null(不能使用 NaN, Infinity, -Infinityundefined)。
    3. 字符串必须使用双引号表示,不能使用单引号。
    4. 对象的键名必须放在双引号里面。
    5. 数组或对象最后一个成员的后面,不能加逗号。
    • JSON.stringfy可以接受三个参数自定义行为。
    • toJSON用于自定义返回值作为参数。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    // 默认输出
    JSON.stringify({ p1: 1, p2: 2 });
    // JSON.stringify({ p1: 1, p2: 2 })
    
    // 分行输出
    JSON.stringify({ p1: 1, p2: 2 }, null, "\t");
    // {
    // 	"p1": 1,
    // 	"p2": 2
    // }
    
    var obj = { a: { b: 1 } };
    
    function f(key, value) {
      console.log("[" + key + "]:" + value);
      return value;
    }
    
    JSON.stringify(obj, f);
    // []:[object Object]
    // [a]:[object Object]
    // [b]:1
    // '{"a":{"b":1}}'
    
    var user = {
      firstName: "三",
      lastName: "张",
    
      get fullName() {
        return this.lastName + this.firstName;
      },
    
      toJSON: function () {
        return {
          name: this.lastName + this.firstName,
        };
      },
    };
    
    JSON.stringify(user);
    // "{"name":"张三"}"
  10. 通过defineProperty实现对象的深拷贝。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function copyObject(orig) {
      return Object.create(
        Object.getPrototypeOf(orig),
        Object.getOwnPropertyDescriptors(orig)
      );
    }
    
    function copyObject(orig) {
      var copy = Object.create(Object.getPrototypeOf(orig));
      copyOwnPropertiesFrom(copy, orig);
      return copy;
    }
    
    function copyOwnPropertiesFrom(target, source) {
      Object.getOwnPropertyNames(source).forEach(function (propKey) {
        var desc = Object.getOwnPropertyDescriptor(source, propKey);
        Object.defineProperty(target, propKey, desc);
      });
      return target;
    }

API

  1. MutationObserver

    • options配置选项。
      childList:子节点的变动(指新增,删除或者更改)。
      attributes:属性的变动。
      characterData:节点内容或节点文本的变动。
      subtree:布尔值,表示是否将该观察器应用于该节点的所有后代节点。
      attributeOldValue:布尔值,表示观察attributes变动时,是否需要记录变动前的属性值。
      characterDataOldValue:布尔值,表示观察characterData变动时,是否需要记录变动前的值。
      attributeFilter:数组,表示需要观察的特定属性(比如['class','src']

    • MutationRecord对象
      MutationRecord 对象包含了 DOM 的相关信息,有如下属性:
      type:观察的变动类型(attributescharacterData或者 childList)。
      target:发生变动的 DOM 节点。
      addedNodes:新增的 DOM 节点。
      removedNodes:删除的 DOM 节点。
      previousSibling:前一个同级节点,如果没有则返回 null
      nextSibling:下一个同级节点,如果没有则返回 null
      attributeName:发生变动的属性。如果设置了 attributeFilter,则只返回预先指定的属性。
      oldValue:变动前的值。这个属性只对 attributecharacterData 变动有效,如果发生 childList 变动,则返回 null

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    // Select the node that will be observed for mutations
    const targetNode = document.getElementById("some-id");
    
    // Options for the observer (which mutations to observe)
    const config = { attributes: true, childList: true, subtree: true };
    
    // Callback function to execute when mutations are observed
    const callback = (mutationList, observer) => {
      for (const mutation of mutationList) {
        if (mutation.type === "childList") {
          console.log("A child node has been added or removed.");
        } else if (mutation.type === "attributes") {
          console.log(`The ${mutation.attributeName} attribute was modified.`);
        }
      }
    };
    
    // Create an observer instance linked to the callback function
    const observer = new MutationObserver(callback);
    
    // Start observing the target node for configured mutations
    observer.observe(targetNode, config);
    
    // Later, you can stop observing
    observer.disconnect();
    
    // 保存所有没有被观察器处理的变动
    var changes = mutationObserver.takeRecords();
    
    // 停止观察
    mutationObserver.disconnect();
  2. addEventListener第三个参数配置。
    其次,第三个参数除了布尔值 useCapture,还可以是一个监听器配置对象,定制事件监听行为。该对象有以下属性。

    capture:布尔值,如果设为 true,表示监听函数在捕获阶段触发,默认为 false,在冒泡阶段触发。
    once:布尔值,如果设为 true,表示监听函数执行一次就会自动移除,后面将不再监听该事件。该属性默认值为 false
    passive:布尔值,设为 true 时,表示禁止监听函数调用 preventDefault()方法。如果调用了,浏览器将忽略这个要求,并在控制台输出一条警告。该属性默认值为 false
    signal:该属性的值为一个 AbortSignal 对象,为监听器设置了一个信号通道,用来在需要时发出信号,移除监听函数。

  3. JS 中的 dispatchEvent 是一个用于在代码中生成和触发事件的方法。它可以用来模拟用户的操作,如点击或按键,或者自定义一些事件,如动画结束或数据更新。使用 dispatchEvent 的步骤如下:

    • 首先,使用 Event 构造函数创建一个新的 Event 对象,指定事件的类型和一些可选的属性,如是否可冒泡或是否可取消。
    • 然后,使用 element.dispatchEvent()方法将事件派发到一个指定的事件目标,如一个元素或一个文档。这个方法会同步地调用所有监听该事件的事件处理函数,并返回一个布尔值,表示事件是否被取消了。
    • 最后,如果需要,可以在事件处理函数中使用 event.preventDefault()方法来阻止事件的默认行为,或者使用 event.stopPropagation()方法来阻止事件的进一步传播。

    下面是一个简单的例子,演示了如何使用 dispatchEvent 来触发一个自定义的 build 事件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 创建一个新的Event对象,指定事件类型为build
    const event = new Event("build");
    
    // 监听build事件
    elem.addEventListener(
      "build",
      (e) => {
        // 在事件处理函数中执行一些操作
        console.log("build event triggered");
      },
      false
    );
    
    // 将build事件派发到elem元素
    elem.dispatchEvent(event);

    Event.stopImmediatePropagation方法阻止同一个事件的其他监听函数被调用,不管监听函数定义在当前节点还是其他节点。也就是说,该方法阻止事件的传播,比Event.stopPropagation()更彻底。

  4. 鼠标事件的种类

    • 点击事件
      • click:按下鼠标(通常是按下主按钮)时触发。
      • dblclick:在同一个元素上双击鼠标时触发。
      • mousedown:按下鼠标键时触发。
      • mouseup:释放按下的鼠标键时触发。
    • 移动事件
      • mousemove:当鼠标在一个节点内部移动时触发。当鼠标持续移动时,该事件会连续触发。为了避免性能问题,建议对该事件的监听函数做一些限定,比如限定一段时间内只能运行一次。
      • mouseenter:鼠标进入一个节点时触发,进入子节点不会触发这个事件。
      • mouseover:鼠标进入一个节点时触发,进入子节点会再一次触发这个事件。
      • mouseout:鼠标离开一个节点时触发,离开父节点也会触发这个事件。
      • mouseleave:鼠标离开一个节点时触发,离开父节点不会触发这个事件。
  5. 键盘事件的种类

    • keydown:按下键盘时触发。
    • keypress:按下有值的键时触发,即按下 CtrlAltShiftMeta 这样无值的键,这个事件不会触发。对于有值的键,按下时先触发 keydown 事件,再触发这个事件。
    • keyup:松开键盘时触发该事件。
  6. 进度事件的种类
    进度事件用来描述资源加载的进度,主要由 AJAX 请求、<img>、<audio>、<video>、<style>、<link>等外部资源的加载触发,继承了 ProgressEvent 接口。它主要包含以下几种事件。

    • abort:外部资源中止加载时(比如用户取消)触发。如果发生错误导致中止,不会触发该事件。
    • error:由于错误导致外部资源无法加载时触发。
    • load:外部资源加载成功时触发。
    • loadstart:外部资源开始加载时触发。
    • loadend:外部资源停止加载时触发,发生顺序排在 error、abort、load 等事件的后面。
    • progress:外部资源加载过程中不断触发。
    • timeout:加载超时时触发。
  7. 表单事件

    • inputinput事件当<input>、<select>、<textarea>的值发生变化时触发。对于复选框(<input type=checkbox>)或单选框(<input type=radio>),用户改变选项时,也会触发这个事件。另外,对于打开 contenteditable 属性的元素,只要值发生变化,也会触发 input 事件。
    • selectselect事件当在<input>、<textarea>里面选中文本时触发。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      // HTML 代码如下
      // <input id="test" type="text" value="Select me!" />
      var elem = document.getElementById("test");
      elem.addEventListener(
        "select",
        function (e) {
          console.log(e.type); // "select"
        },
        false
      );
    • change事件当<input>、<select>、<textarea>的值发生变化时触发。它与input事件的最大不同,就是不会连续触发,只有当全部修改完成时才会触发,另一方面input事件必然伴随change事件。具体来说,分成以下几种情况。

    • invalid:用户提交表单时,如果表单元素的值不满足校验条件,就会触发invalid事件。
  8. 拖拉事件

    • 事件:dragdragstart
    • DataTransfer接口:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    div.addEventListener("drop", function (e) {
      e.preventDefault();
      e.stopPropagation();
    
      var fileList = e.dataTransfer.files;
      if (fileList.length > 0) {
        var file = fileList[0];
        var reader = new FileReader();
        reader.onloadend = function (e) {
          if (e.target.readyState === FileReader.DONE) {
            var content = reader.result;
            div.innerHTML = "File: " + file.name + "\n\n" + content;
          }
        };
        reader.readAsBinaryString(file);
      }
    });
  9. 动态加载脚本防止浏览器假死

    1
    2
    3
    4
    5
    6
    ["a.js", "b.js"].forEach(function (src) {
      var script = document.createElement("script");
      script.src = src;
      script.async = false;
      document.head.appendChild(script);
    });
  10. popstate 事件在浏览器的 history 对象的当前记录发生显式切换时触发。注意,调用 history.pushState()history.replaceState(),并不会触发 popstate 事件。该事件只在用户在 history 记录之间显式切换时触发,比如鼠标点击“后退/前进”按钮,或者在脚本中调用 history.back()history.forward()history.go()时触发。
    该事件对象有一个 state 属性,保存 history.pushState 方法和 history.replaceState 方法为当前记录添加的 state 对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    window.onpopstate = function (event) {
      console.log("state: " + event.state);
    };
    history.pushState({ page: 1 }, "title 1", "?page=1");
    history.pushState({ page: 2 }, "title 2", "?page=2");
    history.replaceState({ page: 3 }, "title 3", "?page=3");
    history.back(); // state: {"page":1}
    history.back(); // state: null
    history.go(2); // state: {"page":3}
  11. hashchange事件在 URLhash 部分(即#号后面的部分,包括#号)发生变化时触发。该事件一般在window对象上监听。
    hashchange的事件实例具有两个特有属性:oldURL属性和newURL属性,分别表示变化前后的完整 URL

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // URL 是 http://www.example.com/
    window.addEventListener("hashchange", myFunction);
    function myFunction(e) {
      console.log(e.oldURL);
      console.log(e.newURL);
    }
    location.hash = "part2";
    // http://www.example.com/
    // http://www.example.com/#part2
  12. 浏览器的弹窗

    • alert:方法弹出的对话框,只有一个“确定”按钮,往往用来通知用户某些信息。用户只有点击“确定”按钮,对话框才会消失。对话框弹出期间,浏览器窗口处于冻结状态,如果不点“确定”按钮,用户什么也干不了。window.alert()方法的参数只能是字符串,没法使用CSS样式,但是可以用\n指定换行。

      1
      2
      window.alert("Hello World");
      alert("本条提示\n分成两行");
    • promptwindow.prompt()方法弹出的对话框,提示文字的下方,还有一个输入框,要求用户输入信息,并有“确定”和“取消”两个按钮。它往往用来获取用户输入的数据。
      window.prompt()的返回值有两种情况,可能是字符串(有可能是空字符串),也有可能是null。具体分成三种情况。

      • 用户输入信息,并点击“确定”,则用户输入的信息就是返回值。
      • 用户没有输入信息,直接点击“确定”,则输入框的默认值就是返回值。
      • 用户点击了“取消”(或者按了ESC按钮),则返回值是null
      1
      var result = prompt("您的年龄?", 25);
    • confirmwindow.confirm()方法弹出的对话框,除了提示信息之外,只有“确定”和“取消”两个按钮,往往用来征询用户是否同意。

      1
      2
      3
      4
      5
      6
      var okay = confirm("Please confirm this message.");
      if (okay) {
        // 用户按下“确定”
      } else {
        // 用户按下“取消”
      }
  13. window窗口的相关的方法

    • open:新建一个浏览器窗口
      1
      2
      3
      4
      5
      var popup = window.open(
        "somepage.html",
        "DefinitionsWindows",
        "height=200,width=200,location=no,status=yes,resizable=yes,scrollbars=yes"
      );
    • close:关闭当前的窗口
    • stop:停止资源的加载
  14. window.navigator属性指向一个包含浏览器和系统信息的Navigator对象。脚本通过这个属性了解用户的环境信息。
  15. ajax的基本使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    //接口地址 https://api.apiopen.top/getJoke
    //获取元素对象
    const btn = document.querySelector("#btn");
    
    btn.addEventListener("click", function () {
      //创建 Promise
      const p = new Promise((resolve, reject) => {
        //1.创建对象
        const xhr = new XMLHttpRequest();
        //2. 初始化
        xhr.open("GET", "https://api.apiopen.top/getJoke");
        //3. 发送
        xhr.send();
        //4. 处理响应结果
        xhr.onreadystatechange = function () {
          if (xhr.readyState === 4) {
            //判断响应状态码 2xx
            if (xhr.status >= 200 && xhr.status < 300) {
              //控制台输出响应体
              resolve(xhr.response);
            } else {
              //控制台输出响应状态码
              reject(xhr.status);
            }
          }
        };
      });
      //调用then方法
      p.then(
        (value) => {
          console.log(value);
        },
        (reason) => {
          console.warn(reason);
        }
      );
    });

浏览器的相关知识

  1. 同源策略(same-origin policy
    要求

    • 协议相同
    • 域名相同
    • 端口相同

    但是,浏览器对于cookie的处理并没有完全遵守同源策略的规定。实际上,浏览器允许同一个域名下的不同端口之间共享cookie。也就是说,如果一个网址是http://www.example.com:8000,它可以读取和写入http://www.example.com:8001的cookie,反之亦然。这是因为浏览器实现来说,“cookie区分域,而不区分端口”。

  2. JSONP的使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function addScriptTag(src) {
      var script = document.createElement("script");
      script.setAttribute("type", "text/javascript");
      script.src = src;
      document.body.appendChild(script);
    }
    window.onload = function () {
      addScriptTag("http://example.com/ip?callback=foo");
    };
    function foo(data) {
      console.log("Your public IP address is: " + data.ip);
    }
    
    foo({
      ip: "8.8.8.8",
    });
  3. CORS的使用

    • 满足简单请求的条件:
      1. 请求方法是以下三种方法之一
        • GET
        • POST
        • HEAD
      2. HTTP 的头信息不超出以下几种字段。
        • Accept
        • Accept-Language
        • Content-Language
        • Last-Event-ID
        • Content-Type:只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain
    • 对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。
    1
    2
    3
    4
    5
    6
    GET /cors HTTP/1.1
    Origin: http://api.bob.com
    Host: api.alice.com
    Accept-Language: en-US
    Connection: keep-alive
    User-Agent: Mozilla/5.0...
    1
    2
    3
    4
    Access-Control-Allow-Origin: http://api.bob.com
    Access-Control-Allow-Credentials: true
    Access-Control-Expose-Headers: FooBar
    Content-Type: text/html; charset=utf-8
    • 相应预检请求的node.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    // 导入http模块
    const http = require("http");
    
    // 创建一个http服务器
    const server = http.createServer((req, res) => {
      // 获取请求方法和请求头
      const method = req.method;
      const headers = req.headers;
    
      // 如果请求方法是OPTIONS,表示是预检请求
      if (method === "OPTIONS") {
        // 设置响应头,允许跨域请求,允许的请求方法和请求头
        res.setHeader("Access-Control-Allow-Origin", "*");
        res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
        res.setHeader(
          "Access-Control-Allow-Headers",
          "Content-Type, Authorization"
        );
        // 结束响应,不返回任何数据
        res.end();
      } else {
        // 如果请求方法不是OPTIONS,表示是正式请求
        // 设置响应头,允许跨域请求,返回数据的类型和编码
        res.setHeader("Access-Control-Allow-Origin", "*");
        res.setHeader("Content-Type", "text/plain; charset=utf-8");
        // 返回一些数据
        res.end("这是一个跨域请求的响应");
      }
    });
    
    // 监听3000端口
    server.listen(3000, () => {
      console.log("服务器启动成功,监听3000端口");
    });
  4. 读取生成URL

    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
    var droptarget = document.getElementById("droptarget");
    droptarget.ondrop = function (e) {
      var files = e.dataTransfer.files;
      for (var i = 0; i < files.length; i++) {
        var type = files[i].type;
        if (type.substring(0, 6) !== "image/") continue;
        var img = document.createElement("img");
        img.src = URL.createObjectURL(files[i]);
        img.onload = function () {
          this.width = 100;
          document.body.appendChild(this);
          URL.revokeObjectURL(this.src);
        };
      }
    };
    // HTML 代码如下
    // <input type="file" onchange="onChange(event)">
    function onChange(event) {
      var file = event.target.files[0];
      var reader = new FileReader();
      reader.onload = function (event) {
        console.log(event.target.result);
      };
      reader.readAsText(file);
    }

ES6

块级作用域

letconst都不会发生变量提升,并且都是块级作用域
这是因为 var 和 let/const 在变量声明时有不同的机制。var 声明的变量会发生变量提升,也就是说,它们的声明会被提升到作用域的顶部,但是赋值操作不会被提升。所以在声明之前使用 var 变量,不会报错,但是变量的值是 undefined,而不是赋值后的值。例如:

1
2
console.log(a); // 输出undefined
var a = 10; // 声明和赋值都被提升

上面的代码相当于:

1
2
3
var a; // 声明提升
console.log(a); // 输出undefined
a = 10; // 赋值保留

而 let/const 声明的变量不会发生变量提升,也就是说,它们的声明和赋值都不会被提升。而且,在声明之前使用 let/const 变量会报错,因为 let/const 声明的变量在初始化之前存在一个暂时性死区(Temporal Dead Zone),在这个区域内,变量不能被访问或者赋值。例如:

1
2
console.log(b); // 报错,b未定义
let b = 10; // 声明提升,赋值不提升

寄生式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 原型链继承
function Animal(name) {
  this.name = name;
}

Animal.prototype.eat = function () {
  console.log(this.name + " is eating.");
};

function Dog(name, breed) {
  Animal.call(this, name); // 调用父类的构造函数
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype); // 设置原型链,让 Dog 继承 Animal
Dog.prototype.constructor = Dog; // 修正构造函数指向
Dog.prototype.bark = function () {
  console.log(this.name + " is barking.");
};

var dog = new Dog("Buddy", "Labrador");
dog.eat(); // 调用从 Animal 继承的方法
dog.bark(); // 调用 Dog 的方法

// ES6 类继承
class Animal {
  constructor(name) {
    this.name = name;
  }

  eat() {
    console.log(this.name + " is eating.");
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 调用父类的构造函数
    this.breed = breed;
  }

  bark() {
    console.log(this.name + " is barking.");
  }
}

const dog = new Dog("Buddy", "Labrador");
dog.eat(); // 调用从 Animal 继承的方法
dog.bark(); // 调用 Dog 的方法

手写一个 new 函数

关键在于如果函数返回了一个对象则以这个对象为new的返回结果。

1
2
3
4
5
const newFunc = (constructor, ...params) => {
  const obj = Object.create(constructor.prototype);
  const result = constructor.apply(obj, params);
  return typeof result === "object" && result !== null ? result : obj;
};

数组的新增方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
const array1 = ["a", "b", "c", "d", "e"];
// Copy to index 0 the element at index 3
console.log(array1.copyWithin(0, 3, 4));
// Expected output: Array ["d", "b", "c", "d", "e"]
// Copy to index 1 all elements from index 3 to the end
console.log(array1.copyWithin(1, 3));
// Expected output: Array ["d", "d", "e", "d", "e"]
// 将3号位复制到0号位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4);
// [4, 2, 3, 4, 5]
// -2相当于3号位,-1相当于4号位
[1, 2, 3, 4, 5].copyWithin(0, -2, -1);
// [4, 2, 3, 4, 5]
// 将3号位复制到0号位
[].copyWithin.call({ length: 5, 3: 1 }, 0, 3);
// {0: 1, 3: 1, length: 5}
// 将2号位到数组结束,复制到0号位
let i32a = new Int32Array([1, 2, 3, 4, 5]);
i32a.copyWithin(0, 2);
// Int32Array [3, 4, 5, 4, 5]
// 对于没有部署 TypedArray 的 copyWithin 方法的平台
// 需要采用下面的写法
[].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4);
// Int32Array [4, 2, 3, 4, 5]

["a", "b", "c"].fill(7);
// [7, 7, 7]
new Array(3).fill(7)[
  // [7, 7, 7]
  ("a", "b", "c")
].fill(7, 1, 2);
// ['a', 7, 'c']
let arr = new Array(3).fill({ name: "Mike" });
arr[0].name = "Ben";
arr;
// [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]
let arr = new Array(3).fill([]);
arr[0].push(5);
arr;
// [[5], [5], [5]]

for (let index of ["a", "b"].keys()) {
  console.log(index);
}
// 0
// 1
for (let elem of ["a", "b"].values()) {
  console.log(elem);
}
// 'a'
// 'b'
for (let [index, elem] of ["a", "b"].entries()) {
  console.log(index, elem);
}
// 0 "a"
// 1 "b"

空值运算符

它的行为类似||,但是只有运算符左侧的值为 nullundefined 时,才会返回右侧的值。

1
2
3
const headerText = response.settings.headerText ?? "Hello, world!";
const animationDuration = response.settings.animationDuration ?? 300;
const showSplashScreen = response.settings.showSplashScreen ?? true;

Symbol

  1. 基本使用

    1
    2
    3
    4
    5
    6
    7
    let s1 = Symbol("foo");
    let s2 = Symbol("bar");
    s1; // Symbol(foo)
    s2; // Symbol(bar)
    s1.toString(); // "Symbol(foo)"
    s2.toString(); // "Symbol(bar)"
    s1.description; // "foo"
  2. 获取Symbol对象属性名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const obj = {};
    let a = Symbol("a");
    let b = Symbol("b");
    obj[a] = "Hello";
    obj[b] = "World";
    const objectSymbols = Object.getOwnPropertySymbols(obj);
    objectSymbols;
    // [Symbol(a), Symbol(b)]
    
    let obj = {
      [Symbol("my_key")]: 1,
      enum: 2,
      nonEnum: 3,
    };
    Reflect.ownKeys(obj);
    //  ["enum", "nonEnum", Symbol(my_key)]
  3. Symbol值的重用

    有时,我们希望重新使用同一个 Symbol 值,Symbol.for()方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局。

    1
    2
    3
    4
    5
    6
    7
    8
    let s1 = Symbol.for("foo");
    let s2 = Symbol.for("foo");
    s1 === s2; // true
    
    let s1 = Symbol.for("foo");
    Symbol.keyFor(s1); // "foo"
    let s2 = Symbol("foo");
    Symbol.keyFor(s2); // undefined

Proxy

  • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.fooproxy['foo']
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = v,返回一个布尔值。
  • has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
  • deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)

  • Reflect配合Proxy实现观察者模式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var obj = new Proxy(
      {},
      {
        get: function (target, propKey, receiver) {
          console.log(`getting ${propKey}!`);
          return Reflect.get(target, propKey, receiver);
        },
        set: function (target, propKey, value, receiver) {
          console.log(`setting ${propKey}!`);
          return Reflect.set(target, propKey, value, receiver);
        },
      }
    );
  • Proxy.revocable()方法返回一个可取消的 Proxy 实例。

    1
    2
    3
    4
    5
    6
    7
    let target = {};
    let handler = {};
    let { proxy, revoke } = Proxy.revocable(target, handler);
    proxy.foo = 123;
    proxy.foo; // 123
    revoke();
    proxy.foo; // TypeError: Revoked

Reflect

Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。

  1. Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在ObjectReflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。
  2. 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false
  3. Object操作都变成函数行为。某些Object操作是命令式,比如name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让它们变成了函数行为。
  4. Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 老写法
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}
// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}

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

Proxy(target, {
  set: function (target, name, value, receiver) {
    var success = Reflect.set(target, name, value, receiver);
    if (success) {
      console.log("property " + name + " on " + target + " set to " + value);
    }
    return success;
  },
});

var loggedObj = new Proxy(obj, {
  get(target, name) {
    console.log("get", target, name);
    return Reflect.get(target, name);
  },
  deleteProperty(target, name) {
    console.log("delete" + name);
    return Reflect.deleteProperty(target, name);
  },
  has(target, name) {
    console.log("has" + name);
    return Reflect.has(target, name);
  },
});

// 老写法
Function.prototype.apply.call(Math.floor, undefined, [1.75]); // 1
// 新写法
Reflect.apply(Math.floor, undefined, [1.75]); // 1

iterator

  1. 简单遍历器实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var it = makeIterator(["a", "b"]);
    it.next(); // { value: "a", done: false }
    it.next(); // { value: "b", done: false }
    it.next(); // { value: undefined, done: true }
    function makeIterator(array) {
      var nextIndex = 0;
      return {
        next: function () {
          return nextIndex < array.length
            ? { value: array[nextIndex++], done: false }
            : { value: undefined, done: true };
        },
      };
    }
  2. return方法的使用场合是,如果for...of循环提前退出(通常是因为出错,或者有break语句),就会调用return方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return方法。

    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
    // return() 方法必须返回一个对象
    function readLinesSync(file) {
      return {
        [Symbol.iterator]() {
          return {
            next() {
              return { done: false };
            },
            return() {
              file.close();
              return { done: true };
            },
          };
        },
      };
    }
    
    // 以下两种情况会导致 return 方法的执行
    // 情况一
    for (let line of readLinesSync(fileName)) {
      console.log(line);
      break;
    }
    // 情况二
    for (let line of readLinesSync(fileName)) {
      console.log(line);
      throw new Error();
    }

generator

  1. 基本实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function* helloWorldGenerator() {
      yield "hello";
      yield "world";
      return "ending";
    }
    var hw = helloWorldGenerator();
    
    hw.next();
    // { value: 'hello', done: false }
    hw.next();
    // { value: 'world', done: false }
    hw.next();
    // { value: 'ending', done: true }
    hw.next();
    // { value: undefined, done: true }
  2. throw方法和return方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    var g = function* () {
      try {
        yield;
      } catch (e) {
        console.log("内部捕获", e);
      }
    };
    var i = g();
    i.next();
    try {
      i.throw("a");
      i.throw("b");
    } catch (e) {
      console.log("外部捕获", e);
    }
    // 内部捕获 a
    // 外部捕获 b
    
    function* gen() {
      yield 1;
      yield 2;
      yield 3;
    }
    var g = gen();
    g.next(); // { value: 1, done: false }
    g.return("foo"); // { value: "foo", done: true }
    g.next(); // { value: undefined, done: true }
    
    function* numbers() {
      yield 1;
      try {
        yield 2;
        yield 3;
      } finally {
        yield 4;
        yield 5;
      }
      yield 6;
    }
    var g = numbers();
    g.next(); // { value: 1, done: false }
    g.next(); // { value: 2, done: false }
    g.return(7); // { value: 4, done: false }
    g.next(); // { value: 5, done: false }
    g.next(); // { value: 7, done: true }

async 函数的实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const asyncFunc = () => {
  return spawn(function* () {});
};
const spawn = (genF) => {
  return new Promise((resolve, reject) => {
    let gen = genF();
    const step = (nextF) => {
      let next;
      try {
        next = nextF();
      } catch (error) {
        reject(error);
      }
      if (next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(
        (v) => step(() => gen.next(v)),
        (r) => step(() => gen.throw(r))
      );
    };
    step(() => gen.next(undefined));
  });
};

super关键字的注意点

由于this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A {
  constructor() {
    this.x = 1;
  }
}
class B extends A {
  constructor() {
    super();
    this.x = 2;
    super.x = 3;
    console.log(super.x); // undefined
    console.log(this.x); // 3
  }
}
let b = new B();

ES6 属性的遍历

ES6 一共有 5 种方法可以遍历对象的属性。

  • for...in:循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)
  • Object.keys(obj):返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名
  • Object.getOwnPropertyNames(obj):回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名
  • Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身的所有 Symbol 属性的键名
  • Reflect.ownKeys(obj):返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举

上述遍历,都遵守同样的属性遍历的次序规则:

  • 首先遍历所有数值键,按照数值升序排列
  • 其次遍历所有字符串键,按照加入时间升序排列
  • 最后遍历所有 Symbol 键,按照加入时间升序排
1
2
Reflect.ownKeys({ [Symbol()]: 0, b: 0, 10: 0, 2: 0, a: 0 });
// ['2', '10', 'b', 'a', Symbol()]