likes
comments
collection
share

学点没用的 JavaScript

作者站长头像
站长
· 阅读数 52

学点没用的 JavaScript

对象

  • 这里有个小陷阱:一个名为 __proto__ 的属性。我们不能将它设置为一个非对象的值:
let obj = {};
obj.__proto__ = 5; // 分配一个数字
alert(obj.__proto__); // [object Object] —— 值为对象,与预期结果不同
  • 对象有顺序吗?换句话说,如果我们遍历一个对象,我们获取属性的顺序是和属性添加时的顺序相同吗?这靠谱吗?
/*
简短的回答是:“有特别的顺序”:整数属性会被进行排序,其他属性则按照创建的顺序显示。详情如下:
例如,让我们考虑一个带有电话号码的对象:
*/
let codes = {
  "49": "Germany",
  "41": "Switzerland",
  "44": "Great Britain",
  // ..,
  "1": "USA"
};

for(let code in codes) {
  alert(code); // 1, 41, 44, 49
}
  • 整数属性?那是什么?
/*
这里的“整数属性”指的是一个可以在不做任何更改的情况下与一个整数进行相互转换的字符串。
所以,"49" 是一个整数属性名,因为我们把它转换成整数,再转换回来,它还是一样的。但是 “+49” 和 “1.2” 就不行了:
*/
// Number(...) 显式转换为数字
// Math.trunc 是内建的去除小数部分的方法。
alert( String(Math.trunc(Number("49"))) ); // "49",相同,整数属性
alert( String(Math.trunc(Number("+49"))) ); // "49",不同于 "+49" ⇒ 不是整数属性
alert( String(Math.trunc(Number("1.2"))) ); // "1",不同于 "1.2" ⇒ 不是整数属性
  • 检查空对象
// 写一个 isEmpty(obj) 函数,当对象没有属性的时候返回 true,否则返回 false。
function isEmpty(obj) {
  for (let key in obj) {
    // 如果进到循环里面,说明有属性。
    return false;
  }
  return true;
}

对象方法,"this"

  • 严格模式下的 this 值为 undefined
function sayHi() {
  alert(this);
}

sayHi(); // undefined
  • 链式调用(这种方法在 JavaScript 库中被广泛使用)
let ladder = {
  step: 0,
  up() {
    this.step++;
    return this;
  },
  down() {
    this.step--;
    return this;
  },
  showStep() {
    alert( this.step );
    return this;
  }
};

ladder.up().up().down().showStep().down().showStep(); // 展示 1,然后 0

构造器和操作符 "new"

  • 如果我们有许多行用于创建单个复杂对象的代码,我们可以将它们封装在一个立即调用的构造函数中,像这样:
// 创建一个函数并立即使用 new 调用它
let user = new function() {
  this.name = "John";
  this.isAdmin = false;

  // ……用于用户创建的其他代码
  // 也许是复杂的逻辑和语句
  // 局部变量等
};
  • 在一个函数内部,我们可以使用 new.target 属性来检查它是否被使用 new 进行调用了。对于常规调用,它为 undefined,对于使用 new 的调用,则等于该函数。

  • 我们也可以让 new 调用和常规调用做相同的工作,像这样(这种方法有时被用在库中以使语法更加灵活):

function User(name) {
  if (!new.target) { // 如果你没有通过 new 运行我
    return new User(name); // ……我会给你添加 new
  }

  this.name = name;
}

let john = User("John"); // 将调用重定向到新用户
alert(john.name); // John
  • 顺便说一下,如果没有参数,我们可以省略 new 后的括号:
let user = new User; // <-- 没有参数
// 等同于
let user = new User();

可选链 "?."

  • 一个例子(“不存在的属性”的问题)
// 如果 document.querySelector('.elem') 的结果为 null,则这里不存在这个元素
let html = document.querySelector('.elem').innerHTML; // 如果 document.querySelector('.elem') 的结果为 null,则会出现错误
  • 如果可选链 ?. 前面的值为 undefined 或者 null,它会停止运算并返回 undefined

  • ?.() 用于调用一个可能不存在的函数。

let userAdmin = {
  admin() {
    alert("I am admin");
  }
};

let userGuest = {};

userAdmin.admin?.(); // I am admin

userGuest.admin?.(); // 啥都没发生(没有这样的方法)
  • 如果我们想使用方括号 [] 而不是点符号 . 来访问属性,语法 ?.[] 也可以使用。
let key = "firstName";

let user1 = {
  firstName: "John"
};

let user2 = null;

alert( user1?.[key] ); // John
alert( user2?.[key] ); // undefined
  • 此外,我们还可以将 ?.delete 一起使用:
delete user?.name; // 如果 user 存在,则删除 user.name
  • 我们应该谨慎地使用 ?.,根据我们的代码逻辑,仅在当左侧部分不存在也可接受的情况下使用为宜。以保证在代码中有编程上的错误出现时,也不会对我们隐藏。

symbol 类型

  • 创建时,我们可以给 symbol 一个描述(也称为 symbol 名),这在代码调试时非常有用:
// id 是描述为 "id" 的 symbol
let id = Symbol("id");
  • symbol 不会被自动转换为字符串
let id = Symbol("id");
alert(id); // 类型错误:无法将 symbol 值转换为字符串。
  • 如果我们真的想显示一个 symbol,我们需要在它上面调用 .toString(),如下所示:
let id = Symbol("id");
alert(id.toString()); // Symbol(id),现在它有效了
  • 或者获取 symbol.description 属性,只显示描述(description):
let id = Symbol("id");
alert(id.description); // id
  • symbol 属性不参与 for..in 循环。Object.keys(obj) 也会忽略它们。相反,Object.assign 会同时复制字符串和 symbol 属性。

  • 全局 symbol

    • Symbol.for(key):从全局 symbol 注册表中读取(不存在则创建)symbol。同名的 symbol 相等。
    • Symbol.keyFor(sym):通过全局 symbol 返回一个名字(不适用于非全局 symbol)。
  • symbol 有两个主要的使用场景:

    • “隐藏” 对象属性。
    • 使用系统 symbol 来改变一些内建行为。
  • 有一个内建方法 Object.getOwnPropertySymbols(obj) 允许我们获取所有的 symbol。还有一个名为 Reflect.ownKeys(obj) 的方法可以返回一个对象的 所有 键,包括 symbol。

let user = {
    name: 'John',
    age: 33,
    [Symbol('aaa')]: 'AAA',
    [Symbol('bbb')]: 'BBB',
};

console.log(Object.keys(user));                     // (2) ['name', 'age']
console.log(Object.getOwnPropertySymbols(user));    // (2) [Symbol(aaa), Symbol(bbb)]
console.log(Reflect.ownKeys(user));                 // (4) ['name', 'age', Symbol(aaa), Symbol(bbb)]

对象 —— 原始值转换

这一章整体很生疏。

let user = {
  name: "John",
  money: 1000,

  [Symbol.toPrimitive](hint) {
    alert(`hint: ${hint}`);
    return hint == "string" ? `{name: "${this.name}"}` : this.money;
  }
};

// 转换演示:
alert(user); // hint: string -> {name: "John"}
alert(+user); // hint: number -> 1000
alert(user + 500); // hint: default -> 1500
let user = {
  name: "John",
  money: 1000,

  // 对于 hint="string"
  toString() {
    return `{name: "${this.name}"}`;
  },

  // 对于 hint="number" 或 "default"
  valueOf() {
    return this.money;
  }

};

alert(user); // toString -> {name: "John"}
alert(+user); // valueOf -> 1000
alert(user + 500); // valueOf -> 1500

对象到原始值的转换,是由许多期望以原始值作为值的内建函数和运算符自动调用的。

这里有三种类型(hint):

  • "string"(对于 alert 和其他需要字符串的操作)
  • "number"(对于数学运算)
  • "default"(少数运算符,通常对象以和 "number" 相同的方式实现 "default" 转换)

规范明确描述了哪个运算符使用哪个 hint。

转换算法是:

  1. 调用 obj[Symbol.toPrimitive](hint) 如果这个方法存在。
  2. 否则,如果 hint 是 "string",尝试调用 obj.toString()obj.valueOf(),无论哪个存在。
  3. 否则,如果 hint 是 "number" 或者 "default",尝试调用 obj.valueOf()obj.toString(),无论哪个存在。

所有这些方法都必须返回一个原始值才能工作(如果已定义)。

在实际使用中,通常只实现 obj.toString() 作为字符串转换的“全能”方法就足够了,该方法应该返回对象的“人类可读”表示,用于日志记录或调试。

数字类型

一个数字以其二进制的形式存储在内存中。在十进制数字系统中,可以保证以 10 的整数次幂作为除数能够正常工作。在二进制数字系统中,可以保证以 2 的整数次幂作为除数时能够正常工作(没有精度损失)。使用二进制数字系统无法 精确 存储 0.1 或 0.2。IEEE-754 数字格式通过将数字舍入到最接近的可能数字来解决此问题。这些舍入规则通常不允许我们看到“极小的精度损失”,但是它确实存在。我们可以看到:alert( 0.1.toFixed(20) ); // 0.10000000000000000555

要写有很多零的数字:

  • "e" 和 0 的数量附加到数字后。就像:123e6123 后面接 6 个 0 相同。
  • "e" 后面的负数将使数字除以 1 后面接着给定数量的零的数字。例如 123e-6 表示 0.000123123 的百万分之一)。

对于不同的数字系统:

  • 可以直接在十六进制(0x),八进制(0o)和二进制(0b)系统中写入数字。
  • parseInt(str, base) 将字符串 str 解析为在给定的 base 数字系统中的整数,2 ≤ base ≤ 36
  • num.toString(base) 将数字转换为在给定的 base 数字系统中的字符串。
    • base=16 用于十六进制颜色,字符编码等,数字可以是 0..9A..F
    • base=2 主要用于调试按位操作,数字可以是 01
    • base=36 是最大进制,数字可以是 0..9A..Z。所有拉丁字母都被用于了表示数字。对于 36 进制来说,一个有趣且有用的例子是,当我们需要将一个较长的数字标识符转换成较短的时候,例如做一个短的 URL。可以简单地使用基数为 36 的数字系统表示:alert( 123456..toString(36) ); // 2n9c

对于常规数字检测:

  • isNaN(value) 将其参数转换为数字,然后检测它是否为 NaN
  • isFinite(value) 将其参数转换为数字,如果它是常规数字,则返回 true,而不是 NaN/Infinity/-Infinity

要将 12pt100px 之类的值转换为数字:

  • 使用 parseInt/parseFloat 进行“软”转换,它从字符串中读取数字,然后返回在发生 error 前可以读取到的值。

小数:

  • 使用 Math.floorMath.ceilMath.truncMath.roundnum.toFixed(precision) 进行舍入。
  • 请确保记住使用小数时会损失精度。

更多数学函数:

  • 需要时请查看 Math 对象。这个库很小,但是可以满足基本的需求。

在下面这个示例中,为什么 6.35 被舍入为 6.3 而不是 6.4

alert( 6.35.toFixed(1) ); // 6.3

// 在内部,6.35 的小数部分是一个无限的二进制。在这种情况下,它的存储会造成精度损失。
// 精度损失可能会导致数字的增加和减小。在这种特殊的情况下,数字变小了一点,这就是它向下舍入的原因。
alert( 6.35.toFixed(20) ); // 6.34999999999999964473

// 如果我们希望以正确的方式进行舍入,我们应该如何解决 6.35 的舍入问题呢?
// 在进行舍入前,我们应该使其更接近整数:
alert( (6.35 * 10).toFixed(20) ); // 63.50000000000000000000

// 请注意,63.5 完全没有精度损失。这是因为小数部分 0.5 实际上是 1/2。以 2 的整数次幂为分母的小数在二进制数字系统中可以被精确地表示,现在我们可以对它进行舍入:
alert( Math.round(6.35 * 10) / 10); // 6.35 -> 63.5 -> 64(rounded) -> 6.4

这是一个无限循环。它永远不会结束。因为对 0.2 这样的小数时进行加法运算时出现了精度损失。结论:在处理小数时避免相等性检查。

let i = 0;
while (i != 10) {
  i += 0.2;
}

创建一个函数 randomInteger(min, max),该函数会生成一个范围在 minmax 中的随机整数,包括 minmax

function randomInteger(min, max) {
  // here rand is from min to (max+1)
  let rand = min + Math.random() * (max + 1 - min);
  return Math.floor(rand);
}

// alert( randomInteger(1, 3) );
// 所有间隔的长度相同,从而使最终能够均匀分配。
// values from 1  ... to 1.9999999999  become 1
// values from 2  ... to 2.9999999999  become 2
// values from 3  ... to 3.9999999999  become 3

字符串

  • 有 3 种类型的引号。反引号允许字符串跨越多行并可以使用 ${…} 在字符串中嵌入表达式。
// 反引号还允许我们在第一个反引号之前指定一个“模版函数”。语法是:func`string`。
// 函数 func 被自动调用,接收字符串和嵌入式表达式,并处理它们。

const person = "Mike";
const age = 28;

function myTag(strings, personExp, ageExp) { // strings 为数组,后面依次为嵌入表达式的值
  const str0 = strings[0]; // "That "
  const str1 = strings[1]; // " is a "
  const str2 = strings[2]; // "."

  const ageStr = ageExp > 99 ? "centenarian" : "youngster";

  // 我们甚至可以返回使用模板字面量构建的字符串
  return `${str0}${personExp}${str1}${ageStr}${str2}`;
}

const output = myTag`That ${person} is a ${age}.`;

console.log(output);
// That Mike is a youngster.
  • JavaScript 中的字符串使用的是 UTF-16 编码。

  • 我们可以使用像 \n 这样的特殊字符或通过使用 \xXX(2个十六进制字符)、\uXXXX(4个十六进制字符)、\u{X…XXXXXX}(1到6个十六进制字符) 来操作它们的 Unicode 进行字符插入。

  • 获取字符时,使用 []charAt。它们之间的唯一区别是,如果没有找到字符,[] 返回 undefined,而 charAt 返回一个空字符串。

  • 获取子字符串,使用 slice(start / end可以为负值) 或 substring(允许 start 大于 end)。

  • 字符串的大/小写转换,使用:toLowerCase/toUpperCase

  • 查找子字符串时,使用 indexOfincludes/startsWith/endsWith 进行简单检查。

// 如果我们对所有存在位置都感兴趣,可以在一个循环中使用 indexOf。
// 每一次新的调用都发生在上一匹配位置之后:

let str = 'As sly as a fox, as strong as an ox';

let target = 'as'; // 这是我们要查找的目标

let pos = 0;
while (true) {
  let foundPos = str.indexOf(target, pos);
  if (foundPos === -1) break;

  alert( `Found at ${foundPos}` );
  pos = foundPos + 1; // 继续从下一个位置查找
}
// 只要记住:if (~str.indexOf(...)) 读作 “if found”。
// 确切地说,由于 ~ 运算符将大数字截断为 32 位,因此存在给出 0 的其他数字,最小的数字是 ~4294967295=0。
// 这使得这种检查只有在字符串没有那么长的情况下才是正确的。

let str = "Widget";

if (~str.indexOf("Widget")) {
  alert( 'Found it!' ); // 正常运行
}
  • 根据语言比较字符串时使用 localeCompare,否则将按字符代码进行比较。

  • 有特殊的方法可以获取代码表示的字符,以及字符对应的代码。前两个可以处理代理对,后两个不行。

    • str.codePointAt(pos) 返回在 pos 位置的字符代码
    • String.fromCodePoint(code) 通过数字 code 创建字符
    • str.charCodeAt(pos) 返回在 pos 位置的字符代码
    • String.fromCharCode(code) 通过数字 code 创建字符
console.log('𩷶'.length)// 2

console.log('𩷶'.codePointAt(0))// 171510
console.log(String.fromCodePoint(171510))// 𩷶

console.log('𩷶'.charCodeAt(0))// 55399
console.log(String.fromCharCode(55399))// 未知字符

console.log('𩷶'.charCodeAt(0).toString(16)) // d867
console.log('𩷶'.charCodeAt(1).toString(16)) // ddf6
console.log('\ud867\uddf6') // 𩷶
  • 变音符号与规范化,讲了 normalize() 方法

数组

我们不应该用 for..in 来处理数组,会有一些潜在问题存在:

  1. for..in 循环会遍历 所有属性,不仅仅是这些数字属性。在浏览器和其它环境中有一种称为“类数组”的对象,它们 看似是数组。也就是说,它们有 length 和索引属性,但是也可能有其它的非数字的属性和方法,这通常是我们不需要的。for..in 循环会把它们都列出来。所以如果我们需要处理类数组对象,这些“额外”的属性就会存在问题。
  2. for..in 循环适用于普通对象,并且做了对应的优化。但是不适用于数组,因此速度要慢 10-100 倍。当然即使是这样也依然非常快。只有在遇到瓶颈时可能会有问题。但是我们仍然应该了解这其中的不同。
const arr = ['aaa', 'bbb', 'ccc']
arr.test = 'test'
arr[3.14] = 'PI'
arr.fn = function () {
}

for (const key in arr) {
    console.log(key) // 0 1 2 test 3.14 fn
}

for (const item of arr) {
    console.log(item) // aaa bbb ccc
}

如果使用单个参数(即数字)调用 new Array,那么它会创建一个 指定了长度,却没有任何项 的数组。

let arr;

arr = new Array(5, 4, 3)
console.log(arr) // (3) [5, 4, 3]

arr = new Array(5)
console.log(arr) // (5) [空 ×5]

数组有自己的 toString 方法的实现,会返回以逗号隔开的元素列表。

let arr = [1, 2, 3];
// 对象 arr 处于 "string" hint 环境,会自动调用 toString 方法将自身转换为原始值
alert( arr ); // 1,2,3
alert( String(arr) === '1,2,3' ); // true
// "default" hint
alert( [] + 1 ); // "1"
alert( [1] + 1 ); // "11"
alert( [1,2] + 1 ); // "1,21"

/*
数组没有 Symbol.toPrimitive,也没有 valueOf,它们只能执行 toString 进行转换,
所以这里 [] 就变成了一个空字符串,[1] 变成了 "1",[1,2] 变成了 "1,2"。
当 "+" 运算符把一些项加到字符串后面时,加号后面的项也会被转换成字符串,所以下一步就会是这样:
*/

alert( "" + 1 ); // "1"
alert( "1" + 1 ); // "11"
alert( "1,2" + 1 ); // "1,21"
// "default" hint
alert( 0 == [] ); // true
alert('0' == [] ); // false

// 在 [] 被转换为 '' 后
alert( 0 == '' ); // true,因为 '' 被转换成了数字 0
alert('0' == '' ); // false,没有进一步的类型转换,是不同的字符串

在数组上下文调用

let arr = ["a", "b"];

arr.push(function() {
  alert( this );
})

/*
arr[2]() 调用从句法来看可以类比于 obj[method](),与 obj 对应的是 arr,与 method 对应的是 2。
所以调用 arr[2] 函数也就是调用对象函数。自然地,它接收 this 引用的对象 arr 然后输出该数组:
*/
arr[2](); // a,b,function(){...}

数组方法

数组方法备忘单:

  • 添加/删除元素:

    • push(...items) —— 向尾端添加元素,

    • pop() —— 从尾端提取一个元素,

    • shift() —— 从首端提取一个元素,

    • unshift(...items) —— 向首端添加元素,

    • splice(pos, deleteCount, ...items) —— 从 pos 开始删除 deleteCount 个元素,并插入 items。(使用 delete 删除数组元素后,元素位置将变为空,并不是 undefined,且数组长度不变)

    • slice(start, end) —— 创建一个新数组,将从索引 start 到索引 end(但不包括 end)的元素复制进去。

    • concat(...items) —— 返回一个新数组:复制当前数组的所有元素,并向其中添加 items。如果 items 中的任意一项是一个数组,那么就取其元素。

      通常,它只复制数组中的元素。其他对象,即使它们看起来像数组一样,但仍然会被作为一个整体添加:

      let arr = [1, 2];
      
      let arrayLike = {
        0: "something",
        length: 1
      };
      
      alert( arr.concat(arrayLike) ); // 1,2,[object Object]
      

      但是,如果类数组对象具有 Symbol.isConcatSpreadable 属性,那么它就会被 concat 当作一个数组来处理:此对象中的元素将被添加:

      let arr = [1, 2];
      
      let arrayLike = {
        0: "something",
        1: "else",
        [Symbol.isConcatSpreadable]: true,
        length: 2
      };
      
      alert( arr.concat(arrayLike) ); // 1,2,something,else
      
  • 搜索元素:

    • indexOf/lastIndexOf(item, pos) —— 从索引 pos 开始搜索 item,搜索到则返回该项的索引,否则返回 -1

    • includes(value) —— 如果数组有 value,则返回 true,否则返回 false

      const arr = [NaN];
      alert( arr.indexOf(NaN) ); // -1(错,应该为 0)
      alert( arr.includes(NaN) );// true(正确)
      
    • find/filter(func) —— 通过 func 过滤元素,返回使 func 返回 true 的第一个值/所有值。

    • findIndexfind 类似,但返回索引而不是值。

  • 遍历元素:

    • forEach(func) —— 对每个元素都调用 func,不返回任何内容。

      ['aaa', 'bbb', 'ccc'].forEach(console.log);
      // aaa 0 (3) ['aaa', 'bbb', 'ccc']
      // bbb 1 (3) ['aaa', 'bbb', 'ccc']
      // ccc 2 (3) ['aaa', 'bbb', 'ccc']
      
  • 转换数组:

    • map(func) —— 根据对每个元素调用 func 的结果创建一个新数组。
    • sort(func) —— 对数组进行原位(in-place)排序,然后返回它。
    • reverse() —— 原位(in-place)反转数组,然后返回它。
    • split/join —— 将字符串转换为数组并返回。
    • reduce/reduceRight(func, initial) —— 通过对每个元素调用 func 计算数组上的单个值,并在调用之间传递中间结果。
  • 其他:

    • Array.isArray(value) 检查 value 是否是一个数组,如果是则返回 true,否则返回 false

请注意,sortreversesplice 方法修改的是数组本身。

这些是最常用的方法,它们覆盖 99% 的用例。但是还有其他几个:

  • arr.some(fn)/arr.every(fn) 检查数组。

    map 类似,对数组的每个元素调用函数 fn。如果任何/所有结果为 true,则返回 true,否则返回 false

    这两个方法的行为类似于 ||&& 运算符:如果 fn 返回一个真值,arr.some() 立即返回 true 并停止迭代其余数组项;如果 fn 返回一个假值,arr.every() 立即返回 false 并停止对其余数组项的迭代。

    我们可以使用 every 来比较数组:

    function arraysEqual(arr1, arr2) {
      return arr1.length === arr2.length && arr1.every((value, index) => value === arr2[index]);
    }
    
    alert( arraysEqual([1, 2], [1, 2])); // true
    
  • arr.fill(value, start, end) —— 从索引 startend,用重复的 value 填充数组。

  • arr.copyWithin(target, start, end) —— 将从位置 startend 的所有元素复制到 自身target 位置(覆盖现有元素)。

  • arr.flat(depth)/arr.flatMap(fn) 从多维数组创建一个新的扁平数组。

  • Array.of(element0[, element1[, …[, elementN]]]) 基于可变数量的参数创建一个新的 Array 实例,而不需要考虑参数的数量或类型。

创建一个构造函数 Calculator,以创建“可扩展”的 calculator 对象。

function Calculator() {

  this.methods = {
    "-": (a, b) => a - b,
    "+": (a, b) => a + b
  };

  this.calculate = function(str) {

    let split = str.split(' '),
      a = +split[0],
      op = split[1],
      b = +split[2];

    if (!this.methods[op] || isNaN(a) || isNaN(b)) {
      return NaN;
    }

    return this.methods[op](a, b);
  };

  this.addMethod = function(name, func) {
    this.methods[name] = func;
  };
}

let calc = new Calculator;
alert( calc.calculate("3 + 7") ); // 10

let powerCalc = new Calculator;
powerCalc.addMethod("*", (a, b) => a * b);
powerCalc.addMethod("/", (a, b) => a / b);
powerCalc.addMethod("**", (a, b) => a ** b);

let result = powerCalc.calculate("2 ** 3");
alert( result ); // 8

编写函数 shuffle(array) 来随机排列数组的元素(shuffle:洗牌)。所有元素顺序应该具有相等的概率。

// 简单的解决方案(概率不相等)
function shuffle(array) {
  array.sort(() => Math.random() - 0.5);
}

// Fisher-Yates shuffle 算法(概率相等)
function shuffle(array) {
  for (let i = array.length - 1; i > 0; i--) {
    let j = Math.floor(Math.random() * (i + 1)); // 从 0 到 i 的随机索引
    [array[i], array[j]] = [array[j], array[i]];
  }
}

Iterable object(可迭代对象)

可以应用 for..of 的对象被称为 可迭代的

  • 技术上来说,可迭代对象必须实现 Symbol.iterator 方法。
    • obj[Symbol.iterator]() 的结果被称为 迭代器(iterator)。由它处理进一步的迭代过程。
    • 一个迭代器必须有 next() 方法,它返回一个 {done: Boolean, value: any} 对象,这里 done:true 表明迭代结束,否则 value 就是下一个值。
let range = {
    from: 'A',
    to: 'Z',

    [Symbol.iterator]() {
        return { // 返回一个迭代器
            current: this.from.codePointAt(0),
            last: this.to.codePointAt(0),

            next() {
                if (this.current <= this.last) {
                    return {done: false, value: String.fromCodePoint(this.current++)}
                } else {
                    return {done: true}
                }
            }
        }
    }
}

for (const item of range) {
    console.log(item) // 字母从 A 到 Z
}
// 使用 range 自身作为迭代器来简化代码
let range = {
    from: 'A',
    to: 'Z',

    [Symbol.iterator]() {
        // 每当一个新的 for..of 启动时都会重置数据
        this.current = this.from.codePointAt(0)
        this.last = this.to.codePointAt(0)
        return this // 自身作为迭代器
    },

    next() {
        if (this.current <= this.last) {
            return {done: false, value: String.fromCodePoint(this.current++)}
        } else {
            return {done: true}
        }
    }
}

for (const item of range) {
    console.log(item) // 字母从 A 到 Z
}
  • Symbol.iterator 方法会被 for..of 自动调用,但我们也可以直接调用它。
let str = "Hello";

// 和 for..of 做相同的事
// for (let char of str) alert(char);

let iterator = str[Symbol.iterator]();

while (true) {
  let result = iterator.next();
  if (result.done) break;
  alert(result.value); // 一个接一个地输出字符
}
  • 内建的可迭代对象例如字符串和数组,都实现了 Symbol.iterator
  • 字符串迭代器能够识别代理对(surrogate pair)。(注:代理对也就是 UTF-16 扩展字符。)
let str = '𝒳😂';
for (let char of str) {
    alert( char ); // 𝒳,然后是 😂
}

有索引属性和 length 属性的对象被称为 类数组对象。这种对象可能还具有其他属性和方法,但是没有数组的内建方法。

let arrayLike = { // 有索引和 length 属性 => 类数组对象
  0: "Hello",
  1: "World",
  length: 2
};

如果我们仔细研究一下规范 —— 就会发现大多数内建方法都假设它们需要处理的是可迭代对象或者类数组对象,而不是“真正的”数组,因为这样抽象度更高。

Array.from(obj[, mapFn, thisArg]) 将可迭代对象或类数组对象 obj 转化为真正的数组 Array,然后我们就可以对它应用数组的方法。可选参数 mapFnthisArg 允许我们将函数应用到每个元素。

str.split 方法不同,Array.from 依赖于字符串的可迭代特性。因此,就像 for..of 一样,可以正确地处理代理对(surrogate pair)。

// 将一个字符串转换为单个字符的数组
let str1 = 'abc'
console.log(str1.split(''))   // (3) ['a', 'b', 'c']
console.log(Array.from(str1)) // (3) ['a', 'b', 'c']

// split 无法处理代理对
let str2 = '😀😁😂'
console.log(str2.split(''))   // (6) ['\uD83D', '\uDE00', '\uD83D', '\uDE01', '\uD83D', '\uDE02']
console.log(Array.from(str2)) // (3) ['😀', '😁', '😂']

我们甚至可以基于 Array.from 创建代理感知(surrogate-aware)的 slice 方法(注:也就是能够处理 UTF-16 扩展字符的 slice 方法):

let str = '😀😁😂'

// 看看以前的 slice 方法
console.log(str.slice(2, 4)) // 😁
console.log(str.slice(2, 5)) // 😁�
console.log(String.prototype === Object.getPrototypeOf(str)) // true

// 覆写 slice 方法
String.prototype.slice = function (start, end) {
    // this 是当前调用此方法的字符串
    return Array.from(this).slice(start, end).join('')
}

// 能够处理 UTF-16 扩展字符的 slice 方法
console.log(str.slice(0, 1)) // 😀
console.log(str.slice(1, 2)) // 😁
console.log(str.slice(2, 3)) // 😂

Map and Set(映射和集合)

Map 迭代

let recipeMap = new Map([
    ['cucumber', 500],
    ['tomatoes', 350],
    ['onion', 50]
])

let iterator1 = recipeMap.keys()
let iterator2 = recipeMap.values()
let iterator3 = recipeMap.entries()

// 以上三个变量既是 [可迭代对象] 又是 [迭代器]
// 因为它们都可以被 for..of 调用,都有 Symbol.iterator 方法和 next 方法
// Symbol.iterator 方法的返回值是 this (自身作为迭代器)

console.log(iterator1[Symbol.iterator]() === iterator1) // true
console.log(iterator2[Symbol.iterator]() === iterator2) // true
console.log(iterator3[Symbol.iterator]() === iterator3) // true

console.log(iterator1.next().value) // cucumber
console.log(iterator1.next().value) // tomatoes
console.log(iterator1.next().value) // onion

while (true) {
    let result = iterator2.next()
    if (result.done) break
    console.log(result.value) // 500 350 50
}

对象、数组、映射 之间的转换

let obj = {name: '孙悟空', age: 18}
let arr = [['name', '猪八戒'], ['age', 28]]
let map = (new Map).set('name', '沙和尚').set('age', 38)

// obj => arr
console.log(Object.entries(obj))
// obj => map
console.log(new Map(Object.entries(obj)))

// arr => obj
console.log(Object.fromEntries(arr))
// arr => map
console.log(new Map(arr))

// map => obj
console.log(Object.fromEntries(map))
// map => arr
console.log(Array.from(map))

WeakMap and WeakSet(弱映射和弱集合)

let john = { name: "John" };

let map = new Map();
map.set(john, "...");

john = null; // 覆盖引用

// john 被存储在了 map 中,
// 我们可以使用 map.keys() 来获取它
let john = { name: "John" };

let weakMap = new WeakMap();
weakMap.set(john, "...");

john = null; // 覆盖引用

// john 被从内存中删除了!

使用案例:额外的数据

用于处理用户访问计数的代码:

let visitsCountMap = new WeakMap(); // weakmap: user => visits count

// 递增用户来访次数
function countUser(user) {
  let count = visitsCountMap.get(user) || 0;
  visitsCountMap.set(user, count + 1);
}

使用案例:缓存

// 📁 cache.js
let cache = new WeakMap();

// 计算并记结果
function process(obj) {
  if (!cache.has(obj)) {
    let result = /* calculate the result for */ obj;

    cache.set(obj, result);
  }

  return cache.get(obj);
}

// 📁 main.js
let obj = {/* some object */};

let result1 = process(obj);
let result2 = process(obj);

// ……稍后,我们不再需要这个对象时:
obj = null;

// 无法获取 cache.size,因为它是一个 WeakMap,
// 要么是 0,或即将变为 0
// 当 obj 被垃圾回收,缓存的数据也会被清除

WeakSet 变“弱(weak)”的同时,它也可以作为额外的存储空间。但并非针对任意数据,而是针对“是/否”的事实。WeakSet 的元素可能代表着有关该对象的某些信息。

例如,我们可以将用户添加到 WeakSet 中,以追踪访问过我们网站的用户:

let visitedSet = new WeakSet();

let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };

visitedSet.add(john); // John 访问了我们
visitedSet.add(pete); // 然后是 Pete
visitedSet.add(john); // John 再次访问

// visitedSet 现在有两个用户了

// 检查 John 是否来访过?
alert(visitedSet.has(john)); // true

// 检查 Mary 是否来访过?
alert(visitedSet.has(mary)); // false

john = null;

// visitedSet 将被自动清理(即自动清除其中已失效的值 john)

解构赋值

数组解构:等号右侧可以是任何可迭代对象

let [a, b, c] = "abc"; // ["a", "b", "c"]
let [one, two, three] = new Set([1, 2, 3]);

// 这种情况下解构赋值是通过迭代右侧的值来完成工作的。
// 这是一种用于对在 = 右侧的值上调用 for..of 并进行赋值的操作的语法糖。

数组解构:我们可以在等号左侧使用任何“可以被赋值的”东西。

let user = {};
[user.name, user.surname] = "John Smith".split(' ');

alert(user.name); // John
alert(user.surname); // Smith

智能函数参数

function showMenu({ title = "Menu", width = 100, height = 200 } = {}) {
  alert( `${title} ${width} ${height}` );
}

showMenu(); // Menu 100 200

日期和时间

Date 对象被转化为数字时,得到的是对应的时间戳,与使用 date.getTime() 的结果相同:

let date = new Date();
alert(+date); // 以毫秒为单位的数值,与使用 date.getTime() 的结果相同

基准测试(Benchmarking):

function diffSubtract(date1, date2) {
  return date2 - date1;
}

function diffGetTime(date1, date2) {
  return date2.getTime() - date1.getTime();
}

function bench(f) {
  let date1 = new Date(0);
  let date2 = new Date();

  let start = Date.now();
  for (let i = 0; i < 100000; i++) f(date1, date2);
  return Date.now() - start;
}

let time1 = 0;
let time2 = 0;

// 在主循环中增加预热环节
bench(diffSubtract);
bench(diffGetTime);

// 开始度量:交替运行 bench(diffSubtract) 和 bench(diffGetTime) 各 10 次
for (let i = 0; i < 10; i++) {
  time1 += bench(diffSubtract);
  time2 += bench(diffGetTime);
}

alert( 'Total time for diffSubtract: ' + time1 );
alert( 'Total time for diffGetTime: ' + time2 );

对字符串调用 Date.parse

// YYYY-MM-DDTHH:mm:ss.sssZ(可选字符 'Z' 为 +-hh:mm 格式的时区。单个字符 Z 代表 UTC+0 时区。)
let ms = Date.parse('2012-01-26T13:51:50.417-07:00');
alert(ms); // 1327611110417  (时间戳)

let date1 = new Date('2017-01-26') // new Date(datestring) 的算法与 Date.parse 所使用的算法相同
let date2 = new Date(2017, 0, 26)

// 注意下面两个变量的默认值不同
console.log(date1) // Thu Jan 26 2017 08:00:00 GMT+0800 (中国标准时间)
console.log(date2) // Thu Jan 26 2017 00:00:00 GMT+0800 (中国标准时间)

显示星期数:

function getWeekDay(date) {
  let days = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];

  return days[date.getDay()];
}

let date = new Date(2014, 0, 3); // 3 Jan 2014
alert( getWeekDay(date) ); // FR

许多天之前是哪个月几号?

function getDateAgo(date, days) {
  let dateCopy = new Date(date); // 复制这个日期

  dateCopy.setDate(date.getDate() - days);
  return dateCopy.getDate();
}

let date = new Date(2015, 0, 2);

alert( getDateAgo(date, 1) ); // 1, (1 Jan 2015)
alert( getDateAgo(date, 2) ); // 31, (31 Dec 2014)
alert( getDateAgo(date, 365) ); // 2, (2 Jan 2014)

某月的最后一天?

function getLastDayOfMonth(year, month) {
  let date = new Date(year, month + 1, 0);
  return date.getDate();
}

alert( getLastDayOfMonth(2012, 0) ); // 31
alert( getLastDayOfMonth(2012, 1) ); // 29(闰年,二月)
alert( getLastDayOfMonth(2013, 1) ); // 28

格式化相对日期:

function formatDate(date) {
  let diff = new Date() - date; // 以毫秒表示的差值

  if (diff < 1000) { // 少于 1 秒
    return 'right now';
  }

  let sec = Math.floor(diff / 1000); // 将 diff 转换为秒

  if (sec < 60) {
    return sec + ' sec. ago';
  }

  let min = Math.floor(diff / 60000); // 将 diff 转换为分钟
  if (min < 60) {
    return min + ' min. ago';
  }

  // 格式化 date
  // 将前置 0 加到一位数 day/month/hours/minutes 前
  let d = date;
  d = [
    '0' + d.getDate(),
    '0' + (d.getMonth() + 1),
    '' + d.getFullYear(),
    '0' + d.getHours(),
    '0' + d.getMinutes()
  ].map(component => component.slice(-2)); // 得到每个组件的后两位

  // 将时间信息和日期组合在一起
 return d.slice(0, 3).join('.') + ' ' + d.slice(3).join(':');
}

alert( formatDate(new Date(new Date - 1)) ); // "right now"

alert( formatDate(new Date(new Date - 30 * 1000)) ); // "30 sec. ago"

alert( formatDate(new Date(new Date - 5 * 60 * 1000)) ); // "5 min. ago"

// 昨天的日期如:
alert( formatDate(new Date(new Date - 86400 * 1000)) );

JSON 方法,toJSON

  • JSON 是一种数据格式,具有自己的独立标准和大多数编程语言的库。
  • JSON 支持 object,array,string,number,boolean 和 null
  • JavaScript 提供序列化(serialize)成 JSON 的方法 JSON.stringify 和解析 JSON 的方法 JSON.parse
  • 这两种方法都支持用于智能读/写的转换函数。
  • 如果一个对象具有 toJSON,那么它会被 JSON.stringify 调用。

JSON.stringify 也可以应用于原始(primitive)数据类型。

// 数字在 JSON 还是数字
alert( JSON.stringify(1) ) // 1

// 字符串在 JSON 中还是字符串,只是被双引号扩起来
alert( JSON.stringify('test') ) // "test"(长度由4变成了6)

alert( JSON.stringify(true) ); // true

JSON.stringify 支持嵌套对象转换,并且可以自动对其进行转换。重要的限制:不得有循环引用。

JSON.stringify 的完整语法是:

let json = JSON.stringify(value[, replacer, space])

让我们包含除了会导致循环引用的 room.occupiedBy 之外的所有属性:

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  participants: [{name: "John"}, {name: "Alice"}],
  place: room // meetup 引用了 room
};

room.occupiedBy = meetup; // room 引用了 meetup

alert( JSON.stringify(meetup, ['title', 'participants', 'place', 'name', 'number']) );
/*
{
  "title":"Conference",
  "participants":[{"name":"John"},{"name":"Alice"}],
  "place":{"number":23}
}
*/
let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  participants: [{name: "John"}, {name: "Alice"}],
  place: room // meetup 引用了 room
};

room.occupiedBy = meetup; // room 引用了 meetup

alert( JSON.stringify(meetup, function replacer(key, value) {
  // 该函数会为每个 (key,value) 对调用并返回“已替换”的值,该值将替换原有的值。如果值被跳过了,则为 undefined。
  // 这里的 this 的值是包含当前属性的对象(当前属性的“父级”)。
  alert(`${key}: ${value}`);
  return (key === 'occupiedBy') ? undefined : value;
}));

/* key:value pairs that come to replacer:
:             [object Object]
title:        Conference
participants: [object Object],[object Object]
0:            [object Object]
name:         John
1:            [object Object]
name:         Alice
place:        [object Object]
number:       23
occupiedBy:   [object Object]
*/

toString 进行字符串转换,对象也可以提供 toJSON 方法来进行 JSON 转换。如果可用,JSON.stringify 会自动调用它。

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  date: new Date(Date.UTC(2017, 0, 1)),
  room
};

alert( JSON.stringify(meetup) );
/*
  {
    "title":"Conference",
    "date":"2017-01-01T00:00:00.000Z", // 所有日期都有一个内建的 toJSON 方法来返回这种类型的字符串
    "room": {"number":23}
  }
*/

实现 MapSet 的序列化:

let obj = {
    name: '孙悟空',
    age: 18,
    map: new Map([['aaa', 'AAA'], ['bbb', 'BBB']]),
    set: new Set(['111', '222', '333'])
}

console.log(JSON.stringify(obj, null, 4))

/*
{
    "name": "孙悟空",
    "age": 18,
    "map": {},
    "set": {}
}
*/

Map.prototype.toJSON = function () {
    return Object.fromEntries(this)
}

Set.prototype.toJSON = function () {
    return Array.from(this)
}

console.log(JSON.stringify(obj, null, 4))

/*
{
    "name": "孙悟空",
    "age": 18,
    "map": {
        "aaa": "AAA",
        "bbb": "BBB"
    },
    "set": [
        "111",
        "222",
        "333"
    ]
}
*/

JSON.parse 使用 reviver

let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';

let meetup = JSON.parse(str, function(key, value) {
  if (key === 'date') return new Date(value);
  return value;
});

alert( meetup.date.getDate() );

递归和堆栈

递归遍历:我们需要一个函数来获取所有薪资的总数。

let company = {
    sales: [{name: 'John', salary: 1000}, {name: 'Alice', salary: 1600}],
    development: {
        sites: [{name: 'Peter', salary: 2000}, {name: 'Alex', salary: 1800}],
        internals: [{name: 'Jack', salary: 1300}]
    }
};

// 用来完成任务的函数
function sumSalaries(department) {
    if (Array.isArray(department)) { // 情况(1)
        return department.reduce((prev, current) => prev + current.salary, 0); // 求数组的和
    }

    // 情况(2)
    return Object.values(department).reduce((sum, subDep) => sum + sumSalaries(subDep), 0);
}

console.log(sumSalaries(company)); // 7700

链表元素 是一个使用以下元素通过递归定义的对象:

  • value
  • next 属性引用下一个 链表元素 或者代表末尾的 null
let list = {
  value: 1,
  next: {
    value: 2,
    next: {
      value: 3,
      next: {
        value: 4,
        next: null
      }
    }
  }
};

斐波那契数:编写一个函数 fib(n) 返回第 n 个斐波那契数。

function fib(n) {
  return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}

alert( fib(3) ); // 2
alert( fib(7) ); // 13
// fib(77); // 超级慢!
// 自下而上的动态规划
function fib(n) {
  let a = 1;
  let b = 1;
  for (let i = 3; i <= n; i++) {
    let c = a + b;
    a = b;
    b = c;
  }
  return b;
}

alert( fib(3) ); // 2
alert( fib(7) ); // 13
alert( fib(77) ); // 5527939700884757

遍历链表:

function printList(list) {
  let tmp = list;

  while (tmp) {
    alert(tmp.value);
    tmp = tmp.next;
  }

}

Rest 参数与 Spread 语法

箭头函数没有 "arguments":如果我们在箭头函数中访问 arguments,访问到的 arguments 并不属于箭头函数,而是属于箭头函数外部的“普通”函数。

function f() {
  let showArg = () => alert(arguments[0]);
  showArg();
}

f(1); // 1

Spread 语法内部使用了迭代器来收集元素,与 for..of 的方式相同。

Array.from(obj)[...obj] 存在一个细微的差别:

  • Array.from 适用于类数组对象也适用于可迭代对象。
  • Spread 语法只适用于可迭代对象。

变量作用域,闭包

Step 1. 变量

在 JavaScript 中,每个运行的函数,代码块 {...} 以及整个脚本,都有一个被称为 词法环境(Lexical Environment) 的内部(隐藏)的关联对象。

词法环境对象由两部分组成:

  1. 环境记录(Environment Record) —— 一个存储所有局部变量作为其属性(包括一些其他信息,例如 this 的值)的对象。
  2. 外部词法环境 的引用,与外部代码相关联。

一个“变量”只是 环境记录 这个特殊的内部对象的一个属性。“获取或修改变量”意味着“获取或修改词法环境的一个属性”。

词法环境是一个规范对象

“词法环境”是一个规范对象(specification object):它只存在于 语言规范 的“理论”层面,用于描述事物是如何工作的。我们无法在代码中获取该对象并直接对其进行操作。

但 JavaScript 引擎同样可以优化它,比如【清除未被使用的变量以节省内存】和执行其他内部技巧等,但显性行为应该是和上述的无差。

Step 2. 函数声明

一个函数其实也是一个值,就像变量一样。(一个“变量”只是 环境记录 这个特殊的内部对象的一个属性。)

不同之处在于函数声明的初始化会被立即完成。

当创建了一个词法环境(Lexical Environment)时,函数声明会立即变为即用型函数(不像 let 那样直到声明处才可用)。

这就是为什么我们甚至可以在声明自身之前调用一个以函数声明(Function Declaration)的方式声明的函数。

Step 3. 内部和外部的词法环境

在一个函数运行时,在调用刚开始时,会自动创建一个新的词法环境以存储这个调用的局部变量和参数。

当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。

如果在任何地方都找不到这个变量,那么在严格模式下就会报错(在非严格模式下,为了向下兼容,给未定义的变量赋值会创建一个全局变量)。

Step 4. 返回函数

所有的函数在“诞生”时都会记住创建它们的词法环境。从技术上讲,这里没有什么魔法:所有函数都有名为 [[Environment]] 的隐藏属性(博主注:我在Edge浏览器看到的是 [[Scopes]]) ,该属性保存了对创建该函数的词法环境的引用。

function makeCounter() {
    let count = 0;
    /*
    正如我们所看到的,理论上当函数可达时,它外部的所有变量也都将存在。

    但在实际中,JavaScript 引擎会试图优化它。它们会分析变量的使用情况,如果从代码中可以明显看出有未使用的外部变量,那么就会将其删除。
    */
    let other1 = 123;
    let other2 = 456;

    return function () {
        return count++;
    };
}

let counter = makeCounter();

console.dir(counter)
/*
> ƒ anonymous()
    > [[Scopes]]: Scopes[2]
        > 0: Closure (makeCounter) {count: 0}
        > 1: Global {window: Window, self: Window, document: document, name: '', location: Location, ...}
*/

console.log(counter())
// 0

console.dir(counter)
/*
> ƒ anonymous()
    > [[Scopes]]: Scopes[2]
        > 0: Closure (makeCounter) {count: 1}
        > 1: Global {window: Window, self: Window, document: document, name: '', location: Location, ...}
*/

console.log(counter())
// 1

因此,counter.[[Scopes]] 有对 {count: 0} 词法环境的引用。这就是函数记住它创建于何处的方式,与函数被在哪儿调用无关。[[Scopes]] 引用在函数创建时被设置并永久保存。

老旧的 "var"

在浏览器中,除非我们使用 modules,否则使用 var 声明的全局函数和变量会成为全局对象的属性。

var 没有块级作用域。用 var 声明的变量,不是函数作用域就是全局作用域。

var 允许重新声明。

var 声明的变量,可以在其声明语句前被使用。var 声明的变量会在函数开头被定义,与它在代码中定义的位置无关。

function sayHi() {
  alert(phrase); // undefined

  phrase = "Hello";

  if (false) {
    var phrase = "Hi"; // 即使进不来,也能提升。声明会被提升,但是赋值不会。
  }

  alert(phrase); // Hello
}
sayHi();

立即调用函数表达式(immediately-invoked function expressions,IIFE)

函数对象,NFE(命名函数表达式)

在 JavaScript 中,函数的类型是对象。一个容易理解的方式是把函数想象成可被调用的“行为对象(action object)”。我们不仅可以调用它们,还能把它们当作对象来处理:增/删属性,按引用传递等。

name —— 函数的名字。通常取自函数定义,但如果函数定义时没设定函数名,JavaScript 会尝试通过函数的上下文猜一个函数名(例如把赋值的变量名取为函数名)。

length —— 函数定义时的入参的个数。Rest 参数不参与计数。

命名函数表达式(NFE,Named Function Expression),指带有名字的函数表达式的术语。

let sayHi = function func(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    func("Guest"); // 使用 func 再次调用函数自身
  }
};

sayHi(); // Hello, Guest

// 但这不工作:
func(); // Error, func is not defined(在函数外不可见)

/*
关于名字 func 有两个特殊的地方,这就是添加它的原因:
1.它允许函数在内部引用自己。
2.它在函数外是不可见的。
*/

任意数量的括号求和:

function sum(firstNum) {
    let currentSum = firstNum

    function fn(num) {
        currentSum += num
        return fn
    }

    fn[Symbol.toPrimitive] = function (hint) {
        console.log(hint) // number
        return currentSum
    }

    return fn
}

console.log(+sum(1)(2) === 3);
console.log(+sum(1)(2)(3) === 6);
console.log(+sum(5)(-1)(2) === 6);
console.log(+sum(6)(-1)(-2)(-3) === 0);
console.log(+sum(0)(1)(2)(3)(4)(5) === 15);

"new Function" 语法

语法:

let func = new Function ([arg1, arg2, ...argN], functionBody);

由于历史原因,参数也可以按逗号分隔符的形式给出。

以下三种声明的含义相同:

new Function('a', 'b', 'return a + b'); // 基础语法
new Function('a,b', 'return a + b'); // 逗号分隔
new Function('a , b', 'return a + b'); // 逗号和空格分隔

使用 new Function 创建的函数,它的 [[Environment]] 指向全局词法环境,而不是函数所在的外部词法环境。因此,我们不能在 new Function 中直接使用外部变量。不过这样是好事,这有助于降低我们代码出错的可能。并且,从代码架构上讲,显式地使用参数传值是一种更好的方法,并且避免了与使用压缩程序而产生冲突的问题。

window.count = 10 // 全局

let count = 20 // 模块

function makeCounter() {
    let count = 30 // 局部

    // 使用 new Function 创建的函数,它的 [[Environment]] 指向全局词法环境,而不是函数所在的外部词法环境。
    return new Function('return count++')
}

let fn = makeCounter()

console.log(fn()) // 10

调度:setTimeout 和 setInterval

  • setTimeout(func, delay, ...args)setInterval(func, delay, ...args) 方法允许我们在 delay 毫秒之后运行 func 一次或以 delay 毫秒为时间间隔周期性运行 func
  • 要取消函数的执行,我们应该调用 clearInterval/clearTimeout,并将 setInterval/setTimeout 返回的值作为入参传入。
  • 嵌套的 setTimeoutsetInterval 用起来更加灵活,允许我们更精确地设置两次执行之间的时间。
/** instead of:
let timerId = setInterval(() => alert('tick'), 2000);
*/

let timerId = setTimeout(function tick() {
  alert('tick');
  timerId = setTimeout(tick, 2000);
}, 2000);
  • 零延时调度 setTimeout(func, 0)(与 setTimeout(func) 相同)用来调度需要尽快执行的调用,但是会在当前脚本执行完成后进行调用。
  • 浏览器会将 setTimeoutsetInterval 的五层或更多层嵌套调用(调用五次之后)的最小延时限制在 4ms。这是历史遗留问题。
let start = Date.now();
let times = [];

setTimeout(function run() {
  times.push(Date.now() - start); // 保存前一个调用的延时

  if (start + 100 < Date.now()) alert(times); // 100 毫秒之后,显示延时信息
  else setTimeout(run); // 否则重新调度
});

// 输出示例:
// 1,1,1,1,5,11,16,21,26,31,36,41,45,49,55,59,64,69,75,81,86,91,96,101

请注意,所有的调度方法都不能 保证 确切的延时。

例如,浏览器内的计时器可能由于许多原因而变慢:

  • CPU 过载。
  • 浏览器页签处于后台模式。
  • 笔记本电脑用的是省电模式。

所有这些因素,可能会将定时器的最小计时器分辨率(最小延迟)增加到 300ms 甚至 1000ms,具体以浏览器及其设置为准。

装饰器模式和转发,call/apply

透明缓存

function slow(x) {
    // 这里可能会有重负载的 CPU 密集型工作
    console.log(`Called with ${x}`)
    return x
}

function cachingDecorator(func) { // 缓存装饰器
    let cache = new Map()

    return function (x) { // 返回缓存包装器
        if (!cache.has(x)) {
            cache.set(x, func(x))
        }
        return cache.get(x)
    }
}

slow = cachingDecorator(slow)

console.log(slow(1)) // slow(1) 被缓存下来了,并返回结果
console.log('Again: ' + slow(1)) // 返回缓存中的 slow(1) 的结果

console.log(slow(2)) // slow(2) 被缓存下来了,并返回结果
console.log('Again: ' + slow(2)) // 返回缓存中的 slow(2) 的结果

使用 “func.call” 设定上下文

let worker = {
    someMethod() {
        return 1
    },
    slow(x) {
        console.log('Called with ' + x)
        return x * this.someMethod()
    }
}

function cachingDecorator(func) {
    let cache = new Map()

    return function (x) {
        if (!cache.has(x)) {
            let result = func.call(this, x) // 现在 "this" 被正确地传递了
            cache.set(x, result)
        }
        return cache.get(x)
    }
}

worker.slow = cachingDecorator(worker.slow) // 现在对其进行缓存

console.log(worker.slow(2)) // 工作正常
console.log(worker.slow(2)) // 工作正常,没有调用原始函数(使用的缓存)

传递多个参数

let worker = {
    slow(min, max) {
        console.log(`Called with ${min},${max}`)
        return min + max
    }
}

function cachingDecorator(func) {
    let cache = new Map()

    return function () {
        let key = [].join.call(arguments, '+') // 方法借用
        if (!cache.has(key)) {
            // Spread 语法 ... 允许将 可迭代对象 args 作为列表传递给 call。
            // apply 只接受 类数组 args。
            // apply 可能会更快,因为大多数 JavaScript 引擎在内部对其进行了优化。
            let result = func.apply(this, arguments) // 等价于 func.call(this, ...arguments)
            cache.set(key, result)
        }
        return cache.get(key)
    }
}

worker.slow = cachingDecorator(worker.slow)

console.log(worker.slow(3, 5)) // works
console.log('Again ' + worker.slow(3, 5)) // same (cached)

为了实现 cachingDecorator,我们研究了以下方法:

  • func.call(context, arg1, arg2…) —— 用给定的上下文和参数调用 func
  • func.apply(context, args) —— 调用 funccontext 作为 this 和类数组的 args 传递给参数列表。

防抖装饰器:

我们举一个实际中的例子。假设用户输入了一些内容,我们想要在用户输入完成时向服务器发送一个请求。

我们没有必要为每一个字符的输入都发送请求。相反,我们想要等一段时间,然后处理整个结果。

在 Web 浏览器中,我们可以设置一个事件处理程序 —— 一个在每次输入内容发生改动时都会调用的函数。通常,监听所有按键输入的事件的处理程序会被调用的非常频繁。但如果我们为这个处理程序做一个 1000ms 的 debounce 处理,它仅会在最后一次输入后的 1000ms 后被调用一次。

function debounce(func, ms) {
  let timeout;
  return function() {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, arguments), ms);
  };
}

节流装饰器:

让我们看看现实生活中的应用程序,以便更好地理解这个需求,并了解它的来源。

例如,我们想要跟踪鼠标移动。

在浏览器中,我们可以设置一个函数,使其在每次鼠标移动时运行,并获取鼠标移动时的指针位置。在使用鼠标的过程中,此函数通常会执行地非常频繁,大概每秒 100 次(每 10 毫秒)。

我们想要在鼠标指针移动时,更新网页上的某些信息。

……但是更新函数 update() 太重了,无法在每个微小移动上都执行。高于每 100ms 更新一次的更新频次也没有意义。

因此,我们将其包装到装饰器中:使用 throttle(update, 100) 作为在每次鼠标移动时运行的函数,而不是原始的 update()。装饰器会被频繁地调用,但是最多每 100ms 将调用转发给 update() 一次。

function throttle(func, ms) {

  let isThrottled = false,
    savedArgs,
    savedThis;

  function wrapper() {

    if (isThrottled) { // (2)
      savedArgs = arguments;
      savedThis = this;
      return;
    }
    isThrottled = true;

    func.apply(this, arguments); // (1)

    setTimeout(function() {
      isThrottled = false; // (3)
      if (savedArgs) {
        wrapper.apply(savedThis, savedArgs);
        savedArgs = savedThis = null;
      }
    }, ms);
  }

  return wrapper;
}

/*
调用 throttle(func, ms) 返回 wrapper。

1. 在第一次调用期间,wrapper 只运行 func 并设置冷却状态(isThrottled = true)。
2. 在冷却状态下,所有调用都被保存在 savedArgs/savedThis 中。请注意,上下文(this)和参数(arguments)都很重要,应该被保存下来。我们需要它们来重现调用。
3. 经过 ms 毫秒后,setTimeout中的函数被触发。冷却状态被移除(isThrottled = false),如果存在被忽略的调用,将使用最后一次调用保存的参数和上下文运行 wrapper。

第 3 步运行的不是 func,而是 wrapper,因为我们不仅需要执行 func,还需要再次进入冷却状态并设置 setTimeout 以重置节流。
*/

函数绑定,bind

当将对象方法作为回调进行传递,例如传递给 setTimeout,这儿会存在一个常见的问题:“丢失 this”。浏览器中的 setTimeout 方法有些特殊:它为函数调用设定了 this=window。在其他类似的情况下,通常 this 会变为 undefined

let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

setTimeout(user.sayHi, 1000); // Hello, undefined!

解决方案:bind

let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

let sayHi = user.sayHi.bind(user);

// 可以在没有对象(译注:与对象分离)的情况下运行它
sayHi(); // Hello, John!

setTimeout(sayHi, 1000); // Hello, John!

// 即使 user 的值在不到 1 秒内发生了改变
// sayHi 还是会使用预先绑定(pre-bound)的值,该值是对旧的 user 对象的引用
user = {
  sayHi() { alert("Another user in setTimeout!"); }
};

如果一个对象有很多方法,并且我们都打算将它们都传递出去,那么我们可以在一个循环中完成所有方法的绑定:

for (let key in user) {
  if (typeof user[key] == 'function') {
    user[key] = user[key].bind(user);
  }
}

bind 的完整语法如下:

let bound = func.bind(context, [arg1], [arg2], ...);

部分(应用)函数(Partial functions):

function mul(a, b) {
    return a * b;
}

let double = mul.bind(null, 2);
console.log(double(3)); // = mul(2, 3) = 6
console.log(double(4)); // = mul(2, 4) = 8
console.log(double(5)); // = mul(2, 5) = 10

let triple = mul.bind(null, 3);
console.log(triple(3)); // = mul(3, 3) = 9
console.log(triple(4)); // = mul(3, 4) = 12
console.log(triple(5)); // = mul(3, 5) = 15

我们可以通过额外的绑定改变 this 吗?

function f() {
  alert(this.name);
}

f = f.bind( {name: "John"} ).bind( {name: "Ann" } );

f(); // John,一个函数不能被重绑定

属性标志和属性描述符

数据属性 的属性描述符:

{
  "value": "John",
  "writable": true,
  "enumerable": true,
  "configurable": true
}

1、Object.getOwnPropertyDescriptor 方法允许查询有关属性的 完整 信息。

语法是:

let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);

2、为了修改标志,我们可以使用 Object.defineProperty

语法是:

Object.defineProperty(obj, propertyName, descriptor)

3、有一个方法 Object.defineProperties(obj, descriptors),允许一次定义多个属性。

语法是:

Object.defineProperties(obj, {
  prop1: descriptor1,
  prop2: descriptor2
  // ...
});

4、要一次获取所有属性描述符,我们可以使用 Object.getOwnPropertyDescriptors(obj) 方法。

5、还有一些限制访问 整个 对象的方法:

  • Object.preventExtensions(obj) 禁止向对象添加新属性。

  • Object.seal(obj) 禁止添加/删除属性。为所有现有的属性设置 configurable: false

  • Object.freeze(obj) 禁止添加/删除/更改属性。为所有现有的属性设置 configurable: false, writable: false

6、还有针对它们的测试:

  • Object.isExtensible(obj) 如果添加属性被禁止,则返回 false,否则返回 true

  • Object.isSealed(obj) 如果添加/删除属性被禁止,并且所有现有的属性都具有 configurable: false 则返回 true

  • Object.isFrozen(obj) 如果添加/删除/更改属性被禁止,并且所有当前属性都是 configurable: false, writable: false,则返回 true

属性的 getter 和 setter

有两种类型的对象属性。

第一种是 数据属性。我们已经知道如何使用它们了。到目前为止,我们使用过的所有属性都是数据属性。

第二种类型的属性是新东西。它是 访问器属性(accessor property)。它们本质上是用于获取和设置值的函数,但从外部代码来看就像常规属性。

访问器属性 的属性描述符:

let user = Object.defineProperty({}, 'name', {
    get() {
        return this._name
    },
    set(value) {
        this._name = value
    },
    enumerable: true,
    configurable: true
})

user.name = 'John'
console.log(user.name)

请注意,一个属性要么是访问器属性(具有 get/set 方法),要么是数据属性(具有 value),但不能两者都是。

其它案例:

let user = {
  name: "John",
  surname: "Smith",

  get fullName() {
    return `${this.name} ${this.surname}`;
  },

  set fullName(value) {
    [this.name, this.surname] = value.split(" ");
  }
};

// set fullName 将以给定值执行
user.fullName = "Alice Cooper";

alert(user.name); // Alice
alert(user.surname); // Cooper
alert(JSON.stringify(user)) // {"name":"Alice","surname":"Cooper","fullName":"Alice Cooper"} // 因为 enumerable 为 true
let user = {
  name: "John",
  surname: "Smith"
};

Object.defineProperty(user, 'fullName', {
  get() {
    return `${this.name} ${this.surname}`;
  },

  set(value) {
    [this.name, this.surname] = value.split(" ");
  }
});

alert(user.fullName); // John Smith

for(let key in user) alert(key); // name, surname

alert(JSON.stringify(user)) // {"name":"John","surname":"Smith"} // 因为 enumerable 为 false,所以没有 fullName
function User(name, birthday) {
  this.name = name;
  this.birthday = birthday;

  // 年龄是根据当前日期和生日计算得出的
  Object.defineProperty(this, "age", {
    get() {
      let todayYear = new Date().getFullYear();
      return todayYear - this.birthday.getFullYear();
    }
  });
}

let john = new User("John", new Date(1992, 6, 1));

alert( john.birthday ); // birthday 是可访问的
alert( john.age );      // ……age 也是可访问的(附加属性)

原型继承

  • 在 JavaScript 中,所有的对象都有一个隐藏的 [[Prototype]] 属性,它要么是另一个对象,要么就是 null
  • 我们可以使用 obj.__proto__ 访问它(历史遗留下来的 getter/setter)。__proto__ 属性有点过时了。它的存在是出于历史的原因,现代编程语言建议我们应该使用函数 Object.getPrototypeOf/Object.setPrototypeOf 来取代 __proto__ 去 get/set 原型。
  • 通过 [[Prototype]] 引用的对象被称为“原型”。
  • 如果我们想要【读取】 obj 的一个属性或者调用一个方法,并且它不存在,那么 JavaScript 就会尝试在原型中查找它。
  • 【写/删除】操作直接在对象上进行,它们不使用原型。访问器(accessor)属性是一个例外,因为赋值(assignment)操作是由 setter 函数处理的。因此,写入此类属性实际上与调用函数相同。
let user = {
    name: "John",
    surname: "Smith",
    set fullName(value) {
        [this.name, this.surname] = value.split(" ");
    },
    get fullName() {
        return `${this.name} ${this.surname}`;
    }
};

let admin = {__proto__: user, isAdmin: true};

console.log(admin.fullName); // John Smith

// 看似是在给 admin 添加自己的 fullName 属性,实际是在调用 admin.fullName("Alice Cooper");
admin.fullName = "Alice Cooper";
// 此时 admin 自身有了 name 和 surname 属性
console.log(admin.hasOwnProperty('name'), admin.hasOwnProperty('surname')); // true true

console.log(admin.fullName); // Alice Cooper,admin 的内容被修改了
console.log(user.fullName);  // John Smith,user 的内容被保护了
  • 如果我们调用 obj.method(),而且 method 是从原型中获取的,this 仍然会引用 obj。因此,方法始终与当前对象一起使用,即使方法是继承的。
  • for..in 循环在其自身和继承的属性上进行迭代。所有其他的键/值获取方法(例如 Object.keysObject.values 等)仅对对象本身起作用。

F.prototype

在本章中,我们简要介绍了为通过构造函数创建的对象设置 [[Prototype]] 的方法。稍后我们将看到更多依赖于此的高级编程模式。

一切都很简单,只需要记住几条重点就可以清晰地掌握了:

  • F.prototype 属性(不要把它与 [[Prototype]] 弄混了)在 new F 被调用时为新对象的 [[Prototype]] 赋值。
  • F.prototype 的值要么是一个对象,要么就是 null:其他值都不起作用。
  • "prototype" 属性仅当设置在一个构造函数上,并通过 new 调用时,才具有这种特殊的影响。

在常规对象上,prototype 没什么特别的:

let user = {
  name: "John",
  prototype: "Bla-bla" // 这里只是普通的属性
};

默认情况下,所有函数都有 F.prototype = {constructor:F},所以我们可以通过访问它的 "constructor" 属性来获取一个对象的构造器。

function User(name) {
  this.name = name;
}

let user = new User('John');
let user2 = new user.constructor('Pete');

alert( user2.name ); // Pete
function User(name) {
    this.name = name;
}

User.prototype = {};

let user = new User('John');
let user2 = new user.constructor('Pete');

console.log(user.name); // John
console.log(user2.name); // undefined

// user 自己没有 constructor
console.log(user.hasOwnProperty('constructor')) // false

// user 的原型(User.prototype = {};)也没有 constructor,显然这是一个空的普通对象
console.log(user.__proto__ === User.prototype) // true

// user 的原型({})的原型(Object.prototype)有 constructor,其值为 Object 自身
console.log(user.__proto__.__proto__.constructor === Object.prototype.constructor) // true
console.log(Object.prototype.constructor === Object) // true

// 所以 new user.constructor('Pete') 相当于 new Object('Pete')

原生的原型

  • 所有的内建对象都遵循相同的模式(pattern):
    • 方法都存储在 prototype 中(Array.prototypeObject.prototypeDate.prototype 等)。
    • 对象本身只存储数据(数组元素、对象属性、日期)。
  • 原始数据类型也将方法存储在包装器对象的 prototype 中:Number.prototypeString.prototypeBoolean.prototype。只有 undefinednull 没有包装器对象。
  • 内建原型可以被修改或被用新的方法填充。但是不建议更改它们。唯一允许的情况可能是,当我们添加一个还没有被 JavaScript 引擎支持,但已经被加入 JavaScript 规范的新标准时,才可能允许这样做。
// 在 js 中,除了 7 种基本数据类型之外,一切皆对象,包括函数、数组等
// 只要是实例,就会有 __proto__ 属性
// 只要是函数(函数都是 Function 的实例),就会有 __proto__ 属性和 prototype 属性(暂不讨论箭头函数)


// 构造函数 Object Array Function
console.log(Object.prototype.__proto__ === null)
console.log(Array.prototype.__proto__ === Object.prototype)
console.log(Function.prototype.__proto__ === Object.prototype)

console.log(Object.__proto__ === Function.prototype)
console.log(Array.__proto__ === Function.prototype)
console.log(Function.__proto__ === Function.prototype)


// 对象实例 数组实例 函数实例
let obj = {}
let arr = []
function fn() {}

console.log(obj.__proto__ === Object.prototype)
console.log(arr.__proto__ === Array.prototype)
console.log(fn.__proto__ === Function.prototype)

原型方法,没有 __proto__ 的对象

  • 要使用给定的原型创建对象,使用:

    • 字面量语法:{ __proto__: ... },允许指定多个属性

    • Object.create(proto, [descriptors]),允许指定属性描述符。

      Object.create 提供了一种简单的方式来浅拷贝对象、原型及其所有属性描述符(descriptors)。

      let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
      

      知识补充:浅拷贝对象及其所有属性描述符

      let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));
      
  • 设置和访问原型的现代方法有:

    • Object.getPrototypeOf(obj) —— 返回对象 obj[[Prototype]](与 __proto__ 的 getter 相同)。

    • Object.setPrototypeOf(obj, proto) —— 将对象 obj[[Prototype]] 设置为 proto(与 __proto__ 的 setter 相同)。

  • 不推荐使用内建的的 __proto__ getter/setter 获取/设置原型,它现在在 ECMA 规范的附录 B 中。

  • 我们还介绍了使用 Object.create(null){ __proto__: null } 创建的无原型的对象。

    这些对象被用作字典,以存储任意(可能是用户生成的)键。

    通常,对象会从 Object.prototype 继承内建的方法和 __proto__ getter/setter,会占用相应的键,且可能会导致副作用。原型为 null 时,对象才真正是空的。

Class 基本语法

基本的类语法看起来像这样:

class MyClass {
  prop = value; // 属性

  constructor(...) { // 构造器
    // ...
  }

  method(...) {} // method

  get something(...) {} // getter 方法
  set something(...) {} // setter 方法

  [Symbol.iterator]() {} // 有计算名称(computed name)的方法(此处为 symbol)
  // ...
}

技术上来说,MyClass 是一个函数(我们提供作为 constructor 的那个),而 methods、getters 和 setters 都被写入了 MyClass.prototype

类继承

  1. 想要扩展一个类 class Child extends Parent:这意味着 Child.prototype.__proto__ 将是 Parent.prototype,所以方法会被继承。

  2. 重写一个 constructor:在使用 this 之前,我们必须在 Childconstructor 中将父 constructor 调用为 super()

  3. 重写一个方法:我们可以在一个 Child 方法中使用 super.method() 来调用 Parent 方法。

  4. 内部:方法在内部的 [[HomeObject]] 属性中记住了它们的类/对象。这就是 super 如何解析父方法的。因此,将一个带有 super 的方法从一个对象复制到另一个对象是不安全的。

  5. 补充:箭头函数没有自己的 thissuper,所以它们能融入到就近的上下文中,像透明似的。

静态属性和静态方法

静态方法被用于实现属于整个类的功能。它与具体的类实例无关。

举个例子, 一个用于进行比较的方法 Article.compare(article1, article2) 或一个工厂(factory)方法 Article.createTodays()

在类声明中,它们都被用关键字 static 进行了标记。

静态属性被用于当我们想要存储类级别的数据时,而不是绑定到实例。

语法如下所示:

class MyClass {
  static property = ...;

  static method() {
    ...
  }
}

从技术上讲,静态声明与直接给类本身赋值相同:

MyClass.property = ...
MyClass.method = ...

静态属性和方法是可被继承的。

对于 class B extends A,类 B[[prototype]] 指向了 AB.[[Prototype]] = A。因此,如果一个字段在 B 中没有找到,会继续在 A 中查找。

私有的和受保护的属性和方法

就面向对象编程(OOP)而言,内部接口与外部接口的划分被称为 封装

它具有以下优点:

一、保护用户,使他们不会误伤自己。

想象一下,有一群开发人员在使用一个咖啡机。这个咖啡机是由“最好的咖啡机”公司制造的,工作正常,但是保护罩被拿掉了。因此内部接口暴露了出来。

所有的开发人员都是文明的 —— 他们按照预期使用咖啡机。但其中的一个人,约翰,他认为自己是最聪明的人,并对咖啡机的内部做了一些调整。然而,咖啡机两天后就坏了。

这肯定不是约翰的错,而是那个取下保护罩并让约翰进行操作的人的错。

编程也一样。如果一个 class 的使用者想要改变那些本不打算被从外部更改的东西 —— 后果是不可预测的。

二、可支持性。

编程的情况比现实生活中的咖啡机要复杂得多,因为我们不只是购买一次。我们还需要不断开发和改进代码。

如果我们严格界定内部接口,那么这个 class 的开发人员可以自由地更改其内部属性和方法,甚至无需通知用户。

如果你是这样的 class 的开发者,那么你会很高兴知道可以安全地重命名私有变量,可以更改甚至删除其参数,因为没有外部代码依赖于它们。

对于用户来说,当新版本问世时,应用的内部可能被进行了全面检修,但如果外部接口相同,则仍然很容易升级。

三、隐藏复杂性。

人们喜欢使用简单的东西。至少从外部来看是这样。内部的东西则是另外一回事了。

程序员也不例外。

当实施细节被隐藏,并提供了简单且有据可查的外部接口时,总是很方便的。

为了隐藏内部接口,我们使用受保护的或私有的属性:

  • 受保护的字段以 _ 开头。这是一个众所周知的约定,不是在语言级别强制执行的。程序员应该只通过它的类和从它继承的类中访问以 _ 开头的字段。
  • 私有字段以 # 开头。JavaScript 确保我们只能从类的内部访问它们。

目前,各个浏览器对私有字段的支持不是很好,但可以用 polyfill 解决。

扩展内建类

// 给 PowerArray 新增了一个方法(可以增加更多)
class PowerArray extends Array {
  isEmpty() {
    return this.length === 0;
  }
}

let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false

let filteredArr = arr.filter(item => item >= 10);
alert(filteredArr); // 10, 50
alert(filteredArr.isEmpty()); // false

知名的 Symbol.species 是个函数值属性,其被构造函数用以创建派生对象。

class PowerArray extends Array {
  isEmpty() {
    return this.length === 0;
  }

  // 内建方法将使用这个作为 constructor
  static get [Symbol.species]() {
    return Array;
  }
}

let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false

// filter 使用 arr.constructor[Symbol.species] 作为 constructor 创建新数组
let filteredArr = arr.filter(item => item >= 10);

// filteredArr 不是 PowerArray,而是 Array
alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function

类检查:"instanceof"

类型检查方法用于返回值
typeof原始数据类型string
{}.toString原始数据类型,内建对象,包含 Symbol.toStringTag 属性的对象string
instanceof对象true/false
objA.isPrototypeOf(objB)对象true/false

正如我们所看到的,从技术上讲,{}.toString 是一种“更高级的” typeof

当我们使用类的层次结构(hierarchy),并想要对该类进行检查,同时还要考虑继承时,这种场景下 instanceof 操作符确实很出色。(补充:可以将 obj instanceof Class 检查改为 Class.prototype.isPrototypeOf(obj)

Symbol.hasInstance 用于判断某对象是否为某构造器的实例。因此你可以用它自定义 instanceof 操作符在某个类上的行为。

// 设置 instanceOf 检查
// 并假设具有 canEat 属性的都是 animal
class Animal {
  static [Symbol.hasInstance](obj) {
    if (obj.canEat) return true;
  }
}

let obj = { canEat: true };

alert(obj instanceof Animal); // true:Animal[Symbol.hasInstance](obj) 被调用

Symbol.toStringTag 内置通用(well-known)symbol 是一个字符串值属性,用于创建对象的默认字符串描述。它由 Object.prototype.toString() 方法内部访问。

let user = {
  [Symbol.toStringTag]: "User"
};
alert( {}.toString.call(user) ); // [object User]


// 特定于环境的对象和类的 toStringTag:
alert( window[Symbol.toStringTag] ); // Window
alert( XMLHttpRequest.prototype[Symbol.toStringTag] ); // XMLHttpRequest
alert( {}.toString.call(window) ); // [object Window]
alert( {}.toString.call(new XMLHttpRequest()) ); // [object XMLHttpRequest]


class ValidatorClass {
  get [Symbol.toStringTag]() {
    return 'Validator';
  }
}
console.log(Object.prototype.toString.call(new ValidatorClass()));
// Expected output: "[object Validator]"

Generator,高级 iteration

generator

  • generator 是通过 generator 函数 function* f(…) {…} 创建的。
  • 在 generator(仅在)内部,存在 yield 操作。
  • 外部代码和 generator 可能会通过 next/yield 调用交换结果。

在现代 JavaScript 中,generator 很少被使用。但有时它们会派上用场,因为函数在执行过程中与调用代码交换数据的能力是非常独特的。而且,当然,它们非常适合创建可迭代对象。

并且,在下一章我们将会学习 async generator,它们被用于在 for await ... of 循环中读取异步生成的数据流(例如,通过网络分页提取 (paginated fetches over a network))。

在 Web 编程中,我们经常使用数据流,因此这是另一个非常重要的使用场景。

异步迭代和 generator

常规的 iterator 和 generator 可以很好地处理那些不需要花费时间来生成的数据。

当我们期望异步地,有延迟地获取数据时,可以使用它们的异步版本,并且使用 for await..of 替代 for..of

异步 iterator 与常规 iterator 在语法上的区别:

Iterable异步 Iterable
提供 iterator 的对象方法Symbol.iteratorSymbol.asyncIterator
next() 返回的值是{value:…, done: true/false}resolve 成 {value:…, done: true/false}Promise

异步 generator 与常规 generator 在语法上的区别:

Generator异步 Generator
声明方式function*async function*
next() 返回的值是{value:…, done: true/false}resolve 成 {value:…, done: true/false}Promise

在 Web 开发中,我们经常会遇到数据流,它们分段流动(flows chunk-by-chunk)。例如,下载或上传大文件。

我们可以使用异步 generator 来处理此类数据。值得注意的是,在一些环境,例如浏览器环境下,还有另一个被称为 Streams 的 API,它提供了特殊的接口来处理此类数据流,转换数据并将数据从一个数据流传递到另一个数据流(例如,从一个地方下载并立即发送到其他地方)。

补充示例:

let range = {
    from: 97,
    to: 122,

    [Symbol.iterator]() {
        return {
            current: this.from,
            last: this.to,

            next() {
                if (this.current <= this.last) {
                    return {done: false, value: String.fromCodePoint(this.current++)}
                }
                return {done: true}
            }
        }
    },

    [Symbol.asyncIterator]() {
        return {
            current: this.from,
            last: this.to,

            async next() {
                if (this.current <= this.last) {
                    await new Promise(resolve => setTimeout(resolve, 300))
                    return {done: false, value: String.fromCodePoint(this.current++ - 32)}
                }
                return {done: true}
            }
        }
    }
};

(async () => {
    let arr = []
    for await (const item of range) {
        arr.push(item)
    }
    console.log(arr) // 大写字母(后输出)
})()

console.log([...range]) // 小写字母(先输出)
let range = {
    from: 97,
    to: 122,

    * [Symbol.iterator]() {
        for (let i = this.from; i <= this.to; i++) {
            yield String.fromCodePoint(i)
        }
    },

    async* [Symbol.asyncIterator]() {
        for (let i = this.from; i <= this.to; i++) {
            await new Promise(resolve => setTimeout(resolve, 300))
            yield String.fromCodePoint(i - 32)
        }
    }
};

(async () => {
    let arr = []
    for await (const item of range) {
        arr.push(item)
    }
    console.log(arr) // 大写字母(后输出)
})()

console.log([...range]) // 小写字母(先输出)
转载自:https://juejin.cn/post/7377195179759206439
评论
请登录