《ECMAScript 6 入门教程》学习笔记 Ⅱ

news/2024/7/10 22:50:19 标签: es6, javascript, 前端

函数的扩展

函数参数的默认值

指定了默认值以后,函数的 length 属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length 属性将失真。

javascript">(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2

上面代码中,length 属性的返回值,等于函数的参数个数减去指定了默认值的参数个数。比如,上面最后一个函数,定义了 3 个参数,其中有一个参数 c 指定了默认值,因此 length 属性等于 3 减去 1,最后得到 2

这是因为 length 属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,后文的 rest 参数也不会计入 length 属性。

如果设置了默认值的参数不是尾参数,那么 length 属性也不再计入后面的参数了。

javascript">(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1

rest 参数

ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用 arguments 对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

javascript">function add(...values) {
  let sum = 0;

  for (var val of values) {
    sum += val;
  }

  return sum;
}

add(2, 5, 3) // 10

上面代码的 add 函数是一个求和函数,利用 rest 参数,可以向该函数传入任意数目的参数。

函数的 length 属性,不包括 rest 参数。

javascript">(function(a) {}).length  // 1
(function(...a) {}).length  // 0
(function(a, ...b) {}).length  // 1

严格模式

从 ES5 开始,函数内部可以设定为严格模式。

javascript">function doSomething(a, b) {
  'use strict';
  // code
}

ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。

这样规定的原因是,函数内部的严格模式,同时适用于函数体和函数参数。但是,函数执行的时候,先执行函数参数,然后再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。

虽然可以先解析函数体代码,再执行参数代码,但是这样无疑就增加了复杂性。因此,标准索性禁止了这种用法,只要参数使用了默认值、解构赋值、或者扩展运算符,就不能显式指定严格模式。

两种方法可以规避这种限制。第一种是设定全局性的严格模式,这是合法的。

javascript">'use strict';

function doSomething(a, b = a) {
  // code
}

第二种是把函数包在一个无参数的立即执行函数里面

javascript">const doSomething = (function () {
  'use strict';
  return function(value = 42) {
    return value;
  };
}());

name 属性

函数的 name 属性,返回该函数的函数名。

javascript">function foo() {}
foo.name // "foo"

这个属性早就被浏览器广泛支持,但是直到 ES6,才将其写入了标准。

Function 构造函数返回的函数实例,name 属性的值为 anonymous

javascript">(new Function).name // "anonymous"

bind 返回的函数,name 属性值会加上 bound 前缀。

javascript">function foo() {};
foo.bind({}).name // "bound foo"

(function(){}).bind({}).name // "bound "

箭头函数

ES6 允许使用“箭头”(=>)定义函数。

javascript">var f = v => v;

// 等同于
var f = function (v) {
  return v;
};

如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。

javascript">var f = () => 5;
// 等同于
var f = function () { return 5 };

var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
  return num1 + num2;
};

如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用 return 语句返回。

javascript">var sum = (num1, num2) => { return num1 + num2; }

由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。

javascript">// 报错
let getTempItem = id => { id: id, name: "Temp" };

// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });

箭头函数可以与变量解构结合使用。

javascript">const full = ({ first, last }) => first + ' ' + last;

// 等同于
function full(person) {
  return person.first + ' ' + person.last;
}

箭头函数使得表达更加简洁。

javascript">const isEven = n => n % 2 === 0;
const square = n => n * n;

箭头函数有几个使用注意点

(1)函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象。

(2)不可以当作构造函数,也就是说,不可以使用 new 命令,否则会抛出一个错误。

(3)不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

(4)不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。

下面是一个部署管道机制(pipeline)的例子,即前一个函数的输出是后一个函数的输入。

javascript">const pipeline = (...funcs) =>
  val => funcs.reduce((a, b) => b(a), val);

const plus1 = a => a + 1;
const mult2 = a => a * 2;
const addThenMult = pipeline(plus1, mult2);

addThenMult(5)
// 12

如果觉得上面的写法可读性比较差,也可以采用下面的写法。

javascript">const plus1 = a => a + 1;
const mult2 = a => a * 2;

mult2(plus1(5))
// 12

箭头函数还有一个功能,就是可以很方便地改写 λ 演算。

javascript">// λ演算的写法
fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))

// ES6的写法
var fix = f => (x => f(v => x(x)(v)))
               (x => f(v => x(x)(v)));

尾调用优化

尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。

以下三种情况,都不属于尾调用。

javascript">// 情况一
function f(x){
  let y = g(x);
  return y;
}

// 情况二
function f(x){
  return g(x) + 1;
}

// 情况三
function f(x){
  g(x);
}

函数调用自身,称为递归。如果尾调用自身,就称为尾递归。

递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。

javascript">function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120

上面代码是一个阶乘函数,计算 n 的阶乘,最多需要保存 n 个调用记录,复杂度 O(n) 。

如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。

javascript">function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5, 1) // 120

函数式编程有一个概念,叫做柯里化(currying),意思是将多参数的函数转换成单参数的形式。

蹦床函数(trampoline)可以将递归执行转为循环执行。

javascript">function trampoline(f) {
  while (f && f instanceof Function) {
    f = f();
  }
  return f;
}

函数参数的尾逗号

javascript">function clownsEverywhere(
  param1,
  param2,
) { /* ... */ }

clownsEverywhere(
  'foo',
  'bar',
);

Function.prototype.toString()

ES2019 对函数实例的 toString() 方法做出了修改。

toString() 方法返回函数代码本身,以前会省略注释和空格。

javascript">function /* foo comment */ foo () {}

foo.toString()
// function foo() {}

上面代码中,函数 foo 的原始代码包含注释,函数名 foo 和圆括号之间有空格,但是 toString() 方法都把它们省略了。

修改后的 toString() 方法,明确要求返回一模一样的原始代码。

javascript">function /* foo comment */ foo () {}

foo.toString()
// "function /* foo comment */ foo () {}"

catch 命令的参数省略

ES2019 允许catch语句省略参数。

javascript">try {
  // ...
} catch {
  // ...
}

数组的扩展

扩展运算符

扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。

下面是扩展运算符取代 apply 方法的一个实际的例子,应用 Math.max 方法,简化求出一个数组最大元素的写法。

javascript">// ES5 的写法
Math.max.apply(null, [14, 3, 77])

// ES6 的写法
Math.max(...[14, 3, 77])

// 等同于
Math.max(14, 3, 77);

上面代码中,由于 JavaScript 不提供求数组最大元素的函数,所以只能套用 Math.max 函数,将数组转为一个参数序列,然后求最大值。有了扩展运算符以后,就可以直接用 Math.max 了。

另一个例子是通过 push 函数,将一个数组添加到另一个数组的尾部。

javascript">// ES5的 写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);

// ES6 的写法
let arr1 = [0, 1, 2];
let arr2 = [3, 4, 5];
arr1.push(...arr2);

扩展运算符的应用

(1)复制数组

扩展运算符提供了复制数组的简便写法。

javascript">const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;

(2)合并数组

扩展运算符提供了数组合并的新写法。

javascript">const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];

// ES5 的合并数组
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]

// ES6 的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]

(3)与解构赋值结合

扩展运算符可以与解构赋值结合起来,用于生成数组。

javascript">const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest  // [2, 3, 4, 5]

const [first, ...rest] = [];
first // undefined
rest  // []

const [first, ...rest] = ["foo"];
first  // "foo"
rest   // []

如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。

(4)字符串

扩展运算符还可以将字符串转为真正的数组。

javascript">[...'hello']
// [ "h", "e", "l", "l", "o" ]

(5)实现了 Iterator 接口的对象

任何定义了遍历器(Iterator)接口的对象(参阅 Iterator 一章),都可以用扩展运算符转为真正的数组。

(6)Map 和 Set 结构,Generator 函数

扩展运算符内部调用的是数据结构的 Iterator 接口,因此只要具有 Iterator 接口的对象,都可以使用扩展运算符,比如 Map 结构。

javascript">let map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

let arr = [...map.keys()]; // [1, 2, 3]

Array.from()

Array.from 方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。

只要是部署了 Iterator 接口的数据结构,Array.from 都能将其转为数组。

javascript">Array.from('hello')
// ['h', 'e', 'l', 'l', 'o']

let namesSet = new Set(['a', 'b'])
Array.from(namesSet) // ['a', 'b']

Array.of()

Array.of 方法用于将一组值,转换为数组。

javascript">Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1

这个方法的主要目的,是弥补数组构造函数 Array() 的不足。因为参数个数的不同,会导致 Array() 的行为有差异。

javascript">Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]

Array.of 总是返回参数值组成的数组。如果没有参数,就返回一个空数组。

Array.of 方法可以用下面的代码模拟实现。

javascript">function ArrayOf(){
  return [].slice.call(arguments);
}

数组实例的 copyWithin()

数组实例的 copyWithin() 方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。

javascript">Array.prototype.copyWithin(target, start = 0, end = this.length)

它接受三个参数:

  • target(必需):从该位置开始替换数据。如果为负值,表示倒数。
  • start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。
  • end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。

这三个参数都应该是数值,如果不是,会自动转为数值。

javascript">[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]

数组实例的 find() 和 findIndex()

数组实例的 find 方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为 true 的成员,然后返回该成员。如果没有符合条件的成员,则返回 undefined

javascript">[1, 5, 10, 15].find(function(value, index, arr) {
  return value > 9;
}) // 10

上面代码中,find 方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。


数组实例的 fill()

fill 方法使用给定值,填充一个数组。

javascript">['a', 'b', 'c'].fill(7)
// [7, 7, 7]

new Array(3).fill(7)
// [7, 7, 7]

fill 方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。

javascript">['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']

数组实例的 entries(),keys() 和 values()

ES6 提供三个新的方法——entries()keys()values()——用于遍历数组。它们都返回一个遍历器对象(详见《Iterator》一章),可以用 for...of 循环进行遍历,唯一的区别是 keys() 是对键名的遍历、values() 是对键值的遍历,entries() 是对键值对的遍历。

javascript">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"

如果不使用 for...of 循环,可以手动调用遍历器对象的 next 方法,进行遍历。

javascript">let letter = ['a', 'b', 'c'];
let entries = letter.entries();
console.log(entries.next().value); // [0, 'a']
console.log(entries.next().value); // [1, 'b']
console.log(entries.next().value); // [2, 'c']

数组实例的 includes()

Array.prototype.includes 方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的 includes 方法类似。ES2016 引入了该方法。

javascript">[1, 2, 3].includes(2)     // true
[1, 2, 3].includes(4)     // false
[1, 2, NaN].includes(NaN) // true

该方法的第二个参数表示搜索的起始位置,默认为 0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为 -4,但数组长度为 3),则会重置为从 0 开始。

下面代码用来检查当前环境是否支持该方法,如果不支持,部署一个简易的替代版本。

javascript">const contains = (() =>
  Array.prototype.includes
    ? (arr, value) => arr.includes(value)
    : (arr, value) => arr.some(el => el === value)
)();
contains(['foo', 'bar'], 'baz'); // => false

另外,Map 和 Set 数据结构有一个 has 方法,需要注意与 includes 区分。

  • Map 结构的 has 方法,是用来查找键名的,比如 Map.prototype.has(key)WeakMap.prototype.has(key)Reflect.has(target, propertyKey)
  • Set 结构的 has 方法,是用来查找值的,比如 Set.prototype.has(value)WeakSet.prototype.has(value)

数组实例的 flat(),flatMap()

数组的成员有时还是数组,Array.prototype.flat() 用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。

javascript">[1, 2, [3, 4]].flat()
// [1, 2, 3, 4]

flat() 默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将 flat() 方法的参数写成一个整数,表示想要拉平的层数,默认为 1。

javascript">[1, 2, [3, [4, 5]]].flat()
// [1, 2, 3, [4, 5]]

[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]

如果原数组有空位,flat() 方法会跳过空位。

flatMap() 方法对原数组的每个成员执行一个函数(相当于执行 Array.prototype.map()),然后对返回值组成的数组执行 flat() 方法。该方法返回一个新数组,不改变原数组。

javascript">// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]

flatMap() 只能展开一层数组。

flatMap() 方法的参数是一个遍历函数,该函数可以接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组。

javascript">arr.flatMap(function callback(currentValue[, index[, array]]) {
  // ...
}[, thisArg])

flatMap() 方法还可以有第二个参数,用来绑定遍历函数里面的 this


数组的空位

数组的空位指,数组的某一个位置没有任何值。比如,Array 构造函数返回的数组都是空位。

javascript">Array(3) // [, , ,]

注意,空位不是 undefined,一个位置的值等于 undefined,依然是有值的。空位是没有任何值,in 运算符可以说明这一点。

Array.prototype.sort() 的排序稳定性

排序稳定性(stable sorting)是排序算法的重要属性,指的是排序关键字相同的项目,排序前后的顺序不变。


对象的扩展

属性的简洁表示法

ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。


属性名表达式

JavaScript 定义对象的属性,有两种方法。

javascript">// 方法一
obj.foo = true;

// 方法二
obj['a' + 'bc'] = 123;

注意,属性名表达式与简洁表示法,不能同时使用,会报错。

javascript">// 报错
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };

// 正确
const foo = 'bar';
const baz = { [foo]: 'abc'};

方法的 name 属性

函数的 name 属性,返回函数名。对象方法也是函数,因此也有 name 属性。


属性的可枚举性和遍历

可枚举性

对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。
Object.getOwnPropertyDescriptor 方法可以获取该属性的描述对象。

javascript">let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
//  {
//    value: 123,
//    writable: true,
//    enumerable: true,
//    configurable: true
//  }

描述对象的 enumerable 属性,称为“可枚举性”,如果该属性为 false,就表示某些操作会忽略当前属性。

目前,有四个操作会忽略 enumerablefalse 的属性。

  • for...in 循环:只遍历对象自身的和继承的可枚举的属性。
  • Object.keys():返回对象自身的所有可枚举的属性的键名。
  • JSON.stringify():只串行化对象自身的可枚举的属性。
  • Object.assign(): 忽略 enumerablefalse 的属性,只拷贝对象自身的可枚举的属性。

另外,ES6 规定,所有 Class 的原型的方法都是不可枚举的。

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

(1)for…in

for...in 循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。

(2)Object.keys(obj)

Object.keys 返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。

(3)Object.getOwnPropertyNames(obj)

Object.getOwnPropertyNames 返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。

(4)Object.getOwnPropertySymbols(obj)

Object.getOwnPropertySymbols 返回一个数组,包含对象自身的所有 Symbol 属性的键名。

(5)Reflect.ownKeys(obj)

Reflect.ownKeys 返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则:

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

super 关键字

我们知道,this 关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字 super,指向当前对象的原型对象。

注意,super 关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。


对象的扩展运算符

对象的解构赋值用于从一个对象取值,相当于将目标对象自身的所有可遍历的(enumerable)、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面。

javascript">let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }

解构赋值必须是最后一个参数,否则会报错。

javascript">let { ...x, y, z } = someObject; // 句法错误
let { x, ...y, ...z } = someObject; // 句法错误

对象的扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。

对象的扩展运算符等同于使用 Object.assign() 方法。

javascript">let aClone = { ...a };
// 等同于
let aClone = Object.assign({}, a);

链判断运算符

编程实务中,如果读取对象内部的某个属性,往往需要判断一下该对象是否存在。比如,要读取 message.body.user.firstName

ES2020 引入了“链判断运算符”(optional chaining operator)?.

javascript">const firstName = message?.body?.user?.firstName || 'default';
const fooValue = myForm.querySelector('input[name=foo]')?.value

上面代码使用了 ?. 运算符,直接在链式调用的时候判断,左侧的对象是否为 nullundefined。如果是的,就不再往下运算,而是返回 undefined

链判断运算符有三种用法:

  • obj?.prop // 对象属性
  • obj?.[expr] // 同上
  • func?.(...args) // 函数或对象方法的调用

下面是这个运算符常见的使用形式,以及不使用该运算符时的等价形式:

javascript">a?.b
// 等同于
a == null ? undefined : a.b

a?.[x]
// 等同于
a == null ? undefined : a[x]

a?.b()
// 等同于
a == null ? undefined : a.b()

a?.()
// 等同于
a == null ? undefined : a()

Null 判断运算符

读取对象属性的时候,如果某个属性的值是 nullundefined,有时候需要为它们指定默认值。常见做法是通过 || 运算符指定默认值。

ES2020 引入了一个新的 Null 判断运算符 ??。它的行为类似 ||,但是只有运算符左侧的值为 nullundefined 时,才会返回右侧的值。

javascript">const headerText = response.settings.headerText ?? 'Hello, world!';
const animationDuration = response.settings.animationDuration ?? 300;
const showSplashScreen = response.settings.showSplashScreen ?? true;

这个运算符的一个目的,就是跟链判断运算符 ?. 配合使用,为 nullundefined 的值设置默认值。

javascript">const animationDuration = response.settings?.animationDuration ?? 300;

现在的规则是,如果多个逻辑运算符一起使用,必须用括号表明优先级,否则会报错。

javascript">(lhs && middle) ?? rhs;
lhs && (middle ?? rhs);

(lhs ?? middle) && rhs;
lhs ?? (middle && rhs);

(lhs || middle) ?? rhs;
lhs || (middle ?? rhs);

(lhs ?? middle) || rhs;
lhs ?? (middle || rhs);

对象的新增方法

Object.is()

ES6 提出“Same-value equality”(同值相等)算法,Object.is 就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。

javascript">Object.is('foo', 'foo')
// true
Object.is({}, {})
// false

不同之处只有两个:一是 +0 不等于 -0,二是 NaN 等于自身。

javascript">+0 === -0 //true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

ES5 可以通过下面的代码,部署 Object.is

javascript">Object.defineProperty(Object, 'is', {
  value: function(x, y) {
    if (x === y) {
      // 针对+0 不等于 -0的情况
      return x !== 0 || 1 / x === 1 / y;
    }
    // 针对NaN的情况
    return x !== x && y !== y;
  },
  configurable: true,
  enumerable: false,
  writable: true
});

Object.assign()

Object.assign 方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。

javascript">const target = { a: 1 };

const source1 = { b: 2 };
const source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

Object.assign 方法的第一个参数是目标对象,后面的参数都是源对象。

注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

javascript">const target = { a: 1, b: 1 };

const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

Object.assign 方法有很多用处:

(1)为对象添加属性

javascript">class Point {
  constructor(x, y) {
    Object.assign(this, {x, y});
  }
}

上面方法通过 Object.assign 方法,将 x 属性和 y 属性添加到 Point 类的对象实例。

(2)为对象添加方法

javascript">Object.assign(SomeClass.prototype, {
  someMethod(arg1, arg2) {
    ···
  },
  anotherMethod() {
    ···
  }
});

// 等同于下面的写法
SomeClass.prototype.someMethod = function (arg1, arg2) {
  ···
};
SomeClass.prototype.anotherMethod = function () {
  ···
};

上面代码使用了对象属性的简洁表示法,直接将两个函数放在大括号中,再使用 assign 方法添加到 SomeClass.prototype 之中。

(3)克隆对象

javascript">function clone(origin) {
  return Object.assign({}, origin);
}

上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。

(4)合并多个对象

(5)为属性指定默认值


Object.getOwnPropertyDescriptors()

出于完整性的考虑,Object.getOwnPropertyDescriptors() 进入标准以后,以后还会新增 Reflect.getOwnPropertyDescriptors() 方法。


__proto__属性,Object.setPrototypeOf(),Object.getPrototypeOf()

__proto__属性(前后各两个下划线),用来读取或设置当前对象的 prototype 对象。目前,所有浏览器(包括 IE11)都部署了这个属性。

javascript">// es5 的写法
const obj = {
  method: function() { ... }
};
obj.__proto__ = someOtherObj;

// es6 的写法
var obj = Object.create(someOtherObj);
obj.method = function() { ... };

如果一个对象本身部署了__proto__属性,该属性的值就是对象的原型。

javascript">Object.getPrototypeOf({ __proto__: null })
// null

Object.setPrototypeOf 方法的作用与__proto__相同,用来设置一个对象的prototype 对象,返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。

javascript">// 格式
Object.setPrototypeOf(object, prototype)

// 用法
const o = Object.setPrototypeOf({}, null);

Object.getPrototypeOf() 方法与 Object.setPrototypeOf 方法配套,用于读取一个对象的原型对象。

下面是一个例子。

javascript">function Rectangle() {
  // ...
}

const rec = new Rectangle();

Object.getPrototypeOf(rec) === Rectangle.prototype
// true

Object.setPrototypeOf(rec, Object.prototype);
Object.getPrototypeOf(rec) === Rectangle.prototype
// false

Object.keys(),Object.values(),Object.entries()

ES5 引入了 Object.keys 方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。

javascript">var obj = { foo: 'bar', baz: 42 };
Object.keys(obj)
// ["foo", "baz"]

ES2017 引入了跟 Object.keys 配套的 Object.valuesObject.entries,作为遍历一个对象的补充手段,供 for...of 循环使用。

Object.values 方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值。

Object.entries() 方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。


Object.fromEntries()

Object.fromEntries() 方法是 Object.entries() 的逆操作,用于将一个键值对数组转为对象。

javascript">Object.fromEntries([
  ['foo', 'bar'],
  ['baz', 42]
])
// { foo: "bar", baz: 42 }

该方法的一个用处是配合 URLSearchParams 对象,将查询字符串转为对象。

javascript">Object.fromEntries(new URLSearchParams('foo=bar&baz=qux'))
// { foo: "bar", baz: "qux" }

Symbol

概述

ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefinednull、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

Symbol 值通过 Symbol 函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。

javascript">let s = Symbol();

typeof s
// "symbol"

注意,Symbol 函数前不能使用 new 命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。

注意,Symbol 函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol 函数的返回值是不相等的。

javascript">// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();

s1 === s2 // false

// 有参数的情况
let s1 = Symbol('foo');
let s2 = Symbol('foo');

s1 === s2 // false

上面代码中,s1s2 都是 Symbol 函数的返回值,而且参数相同,但是它们是不相等的。


Symbol.prototype.description

创建 Symbol 的时候,可以添加一个描述。

javascript">const sym = Symbol('foo');

上面代码中,sym 的描述就是字符串 foo

但是,读取这个描述需要将 Symbol 显式转为字符串,即下面的写法。

javascript">const sym = Symbol('foo');

String(sym) // "Symbol(foo)"
sym.toString() // "Symbol(foo)"

上面的用法不是很方便。ES2019 提供了一个实例属性 description,直接返回 Symbol 的描述。

javascript">const sym = Symbol('foo');

sym.description // "foo"

作为属性名的 Symbol

由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。

javascript">let mySymbol = Symbol();

// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';

// 第二种写法
let a = {
  [mySymbol]: 'Hello!'
};

// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上写法都得到同样结果
a[mySymbol] // "Hello!"

上面代码通过方括号结构和 Object.defineProperty,将对象的属性名指定为一个 Symbol 值。

注意,Symbol 值作为对象属性名时,不能用点运算符。


实例:消除魔术字符串

魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替。

javascript">function getArea(shape, options) {
  let area = 0;

  switch (shape) {
    case 'Triangle': // 魔术字符串
      area = .5 * options.width * options.height;
      break;
    /* ... more code ... */
  }

  return area;
}

getArea('Triangle', { width: 100, height: 100 }); // 魔术字符串

上面代码中,字符串 Triangle 就是一个魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。

常用的消除魔术字符串的方法,就是把它写成一个变量。

javascript">const shapeType = {
  triangle: 'Triangle'
};

function getArea(shape, options) {
  let area = 0;
  switch (shape) {
    case shapeType.triangle:
      area = .5 * options.width * options.height;
      break;
  }
  return area;
}

getArea(shapeType.triangle, { width: 100, height: 100 });

上面代码中,我们把 Triangle 写成 shapeType 对象的 triangle 属性,这样就消除了强耦合。

如果仔细分析,可以发现 shapeType.triangle 等于哪个值并不重要,只要确保不会跟其他 shapeType 属性的值冲突即可。因此,这里就很适合改用 Symbol 值。

javascript">const shapeType = {
  triangle: Symbol()
};

上面代码中,除了将 shapeType.triangle 的值设为一个 Symbol,其他地方都不用修改。


属性名的遍历

Symbol 作为属性名,遍历对象的时候,该属性不会出现在 for...infor...of 循环中,也不会被 Object.keys()Object.getOwnPropertyNames()JSON.stringify() 返回。

但是,它也不是私有属性,有一个 Object.getOwnPropertySymbols() 方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。

javascript">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)]

上面代码是 Object.getOwnPropertySymbols() 方法的示例,可以获取所有 Symbol 属性名。

另一个新的 API,Reflect.ownKeys() 方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。

javascript">let obj = {
  [Symbol('my_key')]: 1,
  enum: 2,
  nonEnum: 3
};

Reflect.ownKeys(obj)
//  ["enum", "nonEnum", Symbol(my_key)]

Symbol.for(),Symbol.keyFor()

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

javascript">let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');

s1 === s2 // true

Symbol.keyFor() 方法返回一个已登记的 Symbol 类型值的 key

注意,Symbol.for() 为 Symbol 值登记的名字,是全局环境的,不管有没有在全局环境运行。


实例:模块的 Singleton 模式

Singleton 模式指的是调用一个类,任何时候返回的都是同一个实例。

对于 Node 来说,模块文件可以看成是一个类。怎么保证每次执行这个模块文件,返回的都是同一个实例呢?

很容易想到,可以把实例放到顶层对象 global

javascript">// mod.js
function A() {
  this.foo = 'hello';
}

if (!global._foo) {
  global._foo = new A();
}

module.exports = global._foo;

然后,加载上面的 mod.js

javascript">const a = require('./mod.js');
console.log(a.foo);

上面代码中,变量 a 任何时候加载的都是 A 的同一个实例。

但是,这里有一个问题,全局变量 global._foo 是可写的,任何文件都可以修改。

javascript">global._foo = { foo: 'world' };

const a = require('./mod.js');
console.log(a.foo);

上面的代码,会使得加载 mod.js 的脚本都失真。

为了防止这种情况出现,我们就可以使用 Symbol。

javascript">// mod.js
const FOO_KEY = Symbol.for('foo');

function A() {
  this.foo = 'hello';
}

if (!global[FOO_KEY]) {
  global[FOO_KEY] = new A();
}

module.exports = global[FOO_KEY];

上面代码中,可以保证 global[FOO_KEY] 不会被无意间覆盖,但还是可以被改写。

javascript">global[Symbol.for('foo')] = { foo: 'world' };

const a = require('./mod.js');

如果键名使用 Symbol 方法生成,那么外部将无法引用这个值,当然也就无法改写。

javascript">// mod.js
const FOO_KEY = Symbol('foo');

// 后面代码相同 ……

上面代码将导致其他脚本都无法引用 FOO_KEY。但这样也有一个问题,就是如果多次执行这个脚本,每次得到的 FOO_KEY 都是不一样的。虽然 Node 会将脚本的执行结果缓存,一般情况下,不会多次执行同一个脚本,但是用户可以手动清除缓存,所以也不是绝对可靠。


内置的 Symbol 值

除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法:

Symbol.hasInstance

对象的 Symbol.hasInstance 属性,指向一个内部方法。当其他对象使用 instanceof 运算符,判断是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)

Symbol.isConcatSpreadable

对象的 Symbol.isConcatSpreadable 属性等于一个布尔值,表示该对象用于 Array.prototype.concat() 时,是否可以展开。

Symbol.species

对象的 Symbol.species 属性,指向一个构造函数。创建衍生对象时,会使用该属性。

Symbol.match

对象的 Symbol.match 属性,指向一个函数。当执行 str.match(myObject) 时,如果该属性存在,会调用它,返回该方法的返回值。

Symbol.replace

对象的 Symbol.replace 属性,指向一个方法,当该对象被 String.prototype.replace 方法调用时,会返回该方法的返回值。

javascript">String.prototype.replace(searchValue, replaceValue)
// 等同于
searchValue[Symbol.replace](this, replaceValue)

Symbol.search

对象的 Symbol.search 属性,指向一个方法,当该对象被 String.prototype.search 方法调用时,会返回该方法的返回值。

Symbol.split

对象的 Symbol.split 属性,指向一个方法,当该对象被 String.prototype.split 方法调用时,会返回该方法的返回值。

javascript">String.prototype.split(separator, limit)
// 等同于
separator[Symbol.split](this, limit)

Symbol.iterator

对象的 Symbol.iterator 属性,指向该对象的默认遍历器方法。

Symbol.toPrimitive

对象的 Symbol.toPrimitive 属性,指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。

Symbol.toPrimitive 被调用时,会接受一个字符串参数,表示当前运算的模式,一共有三种模式。

  • Number:该场合需要转成数值
  • String:该场合需要转成字符串
  • Default:该场合可以转成数值,也可以转成字符串

Symbol.toStringTag

对象的 Symbol.toStringTag 属性,指向一个方法。在该对象上面调用 Object.prototype.toString 方法时,如果这个属性存在,它的返回值会出现在 toString 方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制 [object Object][object Array]object 后面的那个字符串。

ES6 新增内置对象的 Symbol.toStringTag 属性值如下:

  • JSON[Symbol.toStringTag]:‘JSON’
  • Math[Symbol.toStringTag]:‘Math’
  • Module 对象M[Symbol.toStringTag]:‘Module’
  • ArrayBuffer.prototype[Symbol.toStringTag]:‘ArrayBuffer’
  • DataView.prototype[Symbol.toStringTag]:‘DataView’
  • Map.prototype[Symbol.toStringTag]:‘Map’
  • Promise.prototype[Symbol.toStringTag]:‘Promise’
  • Set.prototype[Symbol.toStringTag]:‘Set’
  • %TypedArray%.prototype[Symbol.toStringTag]:'Uint8Array’等
  • WeakMap.prototype[Symbol.toStringTag]:‘WeakMap’
  • WeakSet.prototype[Symbol.toStringTag]:‘WeakSet’
  • %MapIteratorPrototype%[Symbol.toStringTag]:‘Map Iterator’
  • %SetIteratorPrototype%[Symbol.toStringTag]:‘Set Iterator’
  • %StringIteratorPrototype%[Symbol.toStringTag]:‘String Iterator’
  • Symbol.prototype[Symbol.toStringTag]:‘Symbol’
  • Generator.prototype[Symbol.toStringTag]:‘Generator’
  • GeneratorFunction.prototype[Symbol.toStringTag]:‘GeneratorFunction’

Symbol.unscopables

对象的 Symbol.unscopables 属性,指向一个对象。该对象指定了使用 with 关键字时,哪些属性会被 with 环境排除。


Set 和 Map 数据结构

Set

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

Set 本身是一个构造函数,用来生成 Set 数据结构。

javascript">const s = new Set();

[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));

for (let i of s) {
  console.log(i);
}
// 2 3 5 4

向 Set 加入值的时候,不会发生类型转换,所以 5"5" 是两个不同的值。Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是向 Set 加入值时认为 NaN 等于自身,而精确相等运算符认为 NaN 不等于自身。

另外,两个对象总是不相等的。

Set 结构的实例有以下属性:

  • Set.prototype.constructor:构造函数,默认就是 Set 函数。
  • Set.prototype.size:返回 Set 实例的成员总数。

Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法:

  • Set.prototype.add(value):添加某个值,返回 Set 结构本身。
  • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。
  • Set.prototype.clear():清除所有成员,没有返回值。

Set 结构的实例有四个遍历方法,可以用于遍历成员:

  • Set.prototype.keys():返回键名的遍历器
  • Set.prototype.values():返回键值的遍历器
  • Set.prototype.entries():返回键值对的遍历器
  • Set.prototype.forEach():使用回调函数遍历每个成员

需要特别指出的是,Set 的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用 Set 保存一个回调函数列表,调用时就能保证按照添加顺序调用。


WeakSet

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。

首先,WeakSet 的成员只能是对象,而不能是其他类型的值。

ES6 规定 WeakSet 不可遍历。

WeakSet 是一个构造函数,可以使用 new 命令,创建 WeakSet 数据结构。

javascript">const ws = new WeakSet();

作为构造函数,WeakSet 可以接受一个数组或类似数组的对象作为参数。(实际上,任何具有 Iterable 接口的对象,都可以作为 WeakSet 的参数。)该数组的所有成员,都会自动成为 WeakSet 实例对象的成员。

javascript">const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}

WeakSet 结构有以下三个方法:

  • WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。
  • WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。
  • WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例之中。

Map

JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。

注意,只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。

同理,同样的值的两个实例,在 Map 结构中被视为两个键。

Map 结构的实例有以下属性和操作方法:

(1)size 属性

size 属性返回 Map 结构的成员总数。

(2)Map.prototype.set(key, value)

set 方法设置键名 key 对应的键值为 value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。

javascript">const m = new Map();

m.set('edition', 6)        // 键是字符串
m.set(262, 'standard')     // 键是数值
m.set(undefined, 'nah')    // 键是 undefined

set 方法返回的是当前的 Map 对象,因此可以采用链式写法。

javascript">let map = new Map()
  .set(1, 'a')
  .set(2, 'b')
  .set(3, 'c');

(3)Map.prototype.get(key)

get 方法读取 key 对应的键值,如果找不到 key,返回 undefined

javascript">const m = new Map();

const hello = function() {console.log('hello');};
m.set(hello, 'Hello ES6!') // 键是函数

m.get(hello)  // Hello ES6!

(4)Map.prototype.has(key)

has 方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。

(5)Map.prototype.delete(key)

delete 方法删除某个键,返回 true。如果删除失败,返回 false

(6)Map.prototype.clear()

clear 方法清除所有成员,没有返回值。

Map 结构原生提供三个遍历器生成函数和一个遍历方法:

  • Map.prototype.keys():返回键名的遍历器。
  • Map.prototype.values():返回键值的遍历器。
  • Map.prototype.entries():返回所有成员的遍历器。
  • Map.prototype.forEach():遍历 Map 的所有成员。

需要特别注意的是,Map 的遍历顺序就是插入顺序。

与其他数据结构的互相转换:

(1)Map 转为数组

前面已经提过,Map 转为数组最方便的方法,就是使用扩展运算符(...)。

javascript">const myMap = new Map()
  .set(true, 7)
  .set({foo: 3}, ['abc']);
[...myMap]
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]

(2)数组 转为 Map

将数组传入 Map 构造函数,就可以转为 Map。

javascript">new Map([
  [true, 7],
  [{foo: 3}, ['abc']]
])
// Map {
//   true => 7,
//   Object {foo: 3} => ['abc']
// }

(3)Map 转为对象

如果所有 Map 的键都是字符串,它可以无损地转为对象。

javascript">function strMapToObj(strMap) {
  let obj = Object.create(null);
  for (let [k,v] of strMap) {
    obj[k] = v;
  }
  return obj;
}

const myMap = new Map()
  .set('yes', true)
  .set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }

如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。

(4)对象转为 Map

对象转为 Map 可以通过 Object.entries()

javascript">let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj));

(5)Map 转为 JSON

Map 转为 JSON 要区分两种情况。一种情况是,Map 的键名都是字符串,这时可以选择转为对象 JSON。

javascript">function strMapToJson(strMap) {
  return JSON.stringify(strMapToObj(strMap));
}

let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'

另一种情况是,Map 的键名有非字符串,这时可以选择转为数组 JSON。

javascript">function mapToArrayJson(map) {
  return JSON.stringify([...map]);
}

let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'

(6)JSON 转为 Map

JSON 转为 Map,正常情况下,所有键名都是字符串。


WeakMap

WeakMap 结构与 Map 结构类似,也是用于生成键值对的集合。

javascript">// WeakMap 可以使用 set 方法添加成员
const wm1 = new WeakMap();
const key = {foo: 1};
wm1.set(key, 2);
wm1.get(key) // 2

// WeakMap 也可以接受一个数组,
// 作为构造函数的参数
const k1 = [1, 2, 3];
const k2 = [4, 5, 6];
const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]);
wm2.get(k2) // "bar"

WeakMapMap 的区别有两点。

首先,WeakMap 只接受对象作为键名(null 除外),不接受其他类型的值作为键名。

其次,WeakMap 的键名所指向的对象,不计入垃圾回收机制。

注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。

WeakMap 的示例

首先,打开 Node 命令行。

$ node --expose-gc

上面代码中,--expose-gc参数表示允许手动执行垃圾回收机制。

然后,执行下面的代码。

// 手动执行一次垃圾回收,保证获取的内存使用状态准确
> global.gc();
undefined

// 查看内存占用的初始状态,heapUsed 为 4M 左右
> process.memoryUsage();
{ rss: 21106688,
  heapTotal: 7376896,
  heapUsed: 4153936,
  external: 9059 }

> let wm = new WeakMap();
undefined

// 新建一个变量 key,指向一个 5*1024*1024 的数组
> let key = new Array(5 * 1024 * 1024);
undefined

// 设置 WeakMap 实例的键名,也指向 key 数组
// 这时,key 数组实际被引用了两次,
// 变量 key 引用一次,WeakMap 的键名引用了第二次
// 但是,WeakMap 是弱引用,对于引擎来说,引用计数还是1
> wm.set(key, 1);
WeakMap {}

> global.gc();
undefined

// 这时内存占用 heapUsed 增加到 45M 了
> process.memoryUsage();
{ rss: 67538944,
  heapTotal: 7376896,
  heapUsed: 45782816,
  external: 8945 }

// 清除变量 key 对数组的引用,
// 但没有手动清除 WeakMap 实例的键名对数组的引用
> key = null;
null

// 再次执行垃圾回收
> global.gc();
undefined

// 内存占用 heapUsed 变回 4M 左右,
// 可以看到 WeakMap 的键名引用没有阻止 gc 对内存的回收
> process.memoryUsage();
{ rss: 20639744,
  heapTotal: 8425472,
  heapUsed: 3979792,
  external: 8956 }

上面代码中,只要外部的引用消失,WeakMap 内部的引用,就会自动被垃圾回收清除。由此可见,有了 WeakMap 的帮助,解决内存泄漏就会简单很多。


Proxy

概述

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。

javascript">var proxy = new Proxy(target, handler);

Proxy 对象的所有用法,都是上面这种形式,不同的只是 handler 参数的写法。其中,new Proxy() 表示生成一个 Proxy 实例,target 参数表示所要拦截的目标对象,handler 参数也是一个对象,用来定制拦截行为。


Proxy 实例的方法

get()

get 方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。

get 方法的用法,上文已经有一个例子,下面是另一个拦截读取操作的例子。

javascript">var person = {
  name: "张三"
};

var proxy = new Proxy(person, {
  get: function(target, propKey) {
    if (propKey in target) {
      return target[propKey];
    } else {
      throw new ReferenceError("Prop name \"" + propKey + "\" does not exist.");
    }
  }
});

proxy.name // "张三"
proxy.age // 抛出一个错误

上面代码表示,如果访问目标对象不存在的属性,会抛出一个错误。如果没有这个拦截函数,访问不存在的属性,只会返回 undefined

get 方法可以继承。

set()

set 方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。

注意,严格模式下,set 代理如果没有返回 true,就会报错。

apply()

apply 方法拦截函数的调用、callapply 操作。

apply 方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。

javascript">var handler = {
  apply (target, ctx, args) {
    return Reflect.apply(...arguments);
  }
};

has()

has 方法用来拦截 HasProperty 操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是 in 运算符。

has 方法可以接受两个参数,分别是目标对象、需查询的属性名。

下面的例子使用 has 方法隐藏某些属性,不被 in 运算符发现。

javascript">var handler = {
  has (target, key) {
    if (key[0] === '_') {
      return false;
    }
    return key in target;
  }
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);
'_prop' in proxy // false

上面代码中,如果原对象的属性名的第一个字符是下划线,proxy.has就会返回false,从而不会被in运算符发现。

如果原对象不可配置或者禁止扩展,这时has拦截会报错。

值得注意的是,has方法拦截的是HasProperty操作,而不是HasOwnProperty操作,即has方法不判断一个属性是对象自身的属性,还是继承的属性。

construct()

construct 方法用于拦截 new 命令,下面是拦截对象的写法。

javascript">var handler = {
  construct (target, args, newTarget) {
    return new target(...args);
  }
};

construct 方法可以接受三个参数。

  • target:目标对象
  • args:构造函数的参数对象
  • newTarget:创造实例对象时,new命令作用的构造函数(下面例子的p
javascript">var p = new Proxy(function () {}, {
  construct: function(target, args) {
    console.log('called: ' + args.join(', '));
    return { value: args[0] * 10 };
  }
});

(new p(1)).value
// "called: 1"
// 10

construct 方法返回的必须是一个对象,否则会报错。

deleteProperty()

deleteProperty方法用于拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除。

注意,目标对象自身的不可配置(configurable)的属性,不能被deleteProperty方法删除,否则报错。

defineProperty()

defineProperty方法拦截了Object.defineProperty操作。

javascript">var handler = {
  defineProperty (target, key, descriptor) {
    return false;
  }
};
var target = {};
var proxy = new Proxy(target, handler);
proxy.foo = 'bar' // 不会生效

上面代码中,defineProperty方法返回false,导致添加新属性总是无效。

注意,如果目标对象不可扩展(non-extensible),则defineProperty不能增加目标对象上不存在的属性,否则会报错。另外,如果目标对象的某个属性不可写(writable)或不可配置(configurable),则defineProperty方法不得改变这两个设置。

getOwnPropertyDescriptor()

getOwnPropertyDescriptor方法拦截Object.getOwnPropertyDescriptor(),返回一个属性描述对象或者undefined

getPrototypeOf()

getPrototypeOf方法主要用来拦截获取对象原型。具体来说,拦截下面这些操作:

  • Object.prototype.__proto__
  • Object.prototype.isPrototypeOf()
  • Object.getPrototypeOf()
  • Reflect.getPrototypeOf()
  • instanceof

isExtensible()

isExtensible方法拦截Object.isExtensible操作。

注意,该方法只能返回布尔值,否则返回值会被自动转为布尔值。

ownKeys()

ownKeys方法用来拦截对象自身属性的读取操作。具体来说,拦截以下操作:

  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.keys()
  • for...in 循环

注意,使用Object.keys方法时,有三类属性会被ownKeys方法自动过滤,不会返回:

  • 目标对象上不存在的属性
  • 属性名为 Symbol 值
  • 不可遍历(enumerable)的属性

ownKeys方法还可以拦截Object.getOwnPropertyNames()

for...in 循环也受到ownKeys方法的拦截。

ownKeys方法返回的数组成员,只能是字符串或 Symbol 值。如果有其他类型的值,或者返回的根本不是数组,就会报错。

如果目标对象自身包含不可配置的属性,则该属性必须被ownKeys方法返回,否则报错。

另外,如果目标对象是不可扩展的(non-extensible),这时ownKeys方法返回的数组之中,必须包含原对象的所有属性,且不能包含多余的属性,否则报错。

preventExtensions()

preventExtensions方法拦截Object.preventExtensions()。该方法必须返回一个布尔值,否则会被自动转为布尔值。

这个方法有一个限制,只有目标对象不可扩展时(即Object.isExtensible(proxy)false),proxy.preventExtensions才能返回true,否则会报错。

javascript">var proxy = new Proxy({}, {
  preventExtensions: function(target) {
    return true;
  }
});

Object.preventExtensions(proxy)
// Uncaught TypeError: 'preventExtensions' on proxy: trap returned truish but the proxy target is extensible

上面代码中,proxy.preventExtensions方法返回true,但这时Object.isExtensible(proxy)会返回true,因此报错。

为了防止出现这个问题,通常要在proxy.preventExtensions方法里面,调用一次Object.preventExtensions

javascript">var proxy = new Proxy({}, {
  preventExtensions: function(target) {
    console.log('called');
    Object.preventExtensions(target);
    return true;
  }
});

Object.preventExtensions(proxy)
// "called"
// Proxy {}

setPrototypeOf()

setPrototypeOf方法主要用来拦截Object.setPrototypeOf方法。

注意,该方法只能返回布尔值,否则会被自动转为布尔值。另外,如果目标对象不可扩展(non-extensible),setPrototypeOf方法不得改变目标对象的原型。


Proxy.revocable()

Proxy.revocable方法返回一个可取消的 Proxy 实例。

javascript">let target = {};
let handler = {};

let {proxy, revoke} = Proxy.revocable(target, handler);

proxy.foo = 123;
proxy.foo // 123

revoke();
proxy.foo // TypeError: Revoked

Proxy.revocable方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。上面代码中,当执行revoke函数之后,再访问Proxy实例,就会抛出一个错误。

Proxy.revocable的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。


this 问题

虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理。

此外,有些原生对象的内部属性,只有通过正确的this才能拿到,所以 Proxy 也无法代理这些原生对象的属性。


实例:Web 服务的客户端

Proxy 对象可以拦截目标对象的任意属性,这使得它很合适用来写 Web 服务的客户端。

javascript">const service = createWebService('http://example.com/data');

service.employees().then(json => {
  const employees = JSON.parse(json);
  // ···
});

上面代码新建了一个 Web 服务的接口,这个接口返回各种数据。Proxy 可以拦截这个对象的任意属性,所以不用为每一种数据写一个适配方法,只要写一个 Proxy 拦截就可以了。

javascript">function createWebService(baseUrl) {
  return new Proxy({}, {
    get(target, propKey, receiver) {
      return () => httpGet(baseUrl + '/' + propKey);
    }
  });
}

同理,Proxy 也可以用来实现数据库的 ORM 层。


http://www.niftyadmin.cn/n/1770228.html

相关文章

python property方法_python property方法的使用

property的作用:实例:class C:def __init__(self, size 10):self.size sizedef getSize(self):return self.sizedef setSize(self, value):self.size valuedef delSize(self):del self.sizex property(getSize, setSize, delSize)c C()print(c.getS…

js网页时钟(附素材抠图)

效果图如下&#xff1a; 代码 <!DOCTYPE html><meta http-equiv"Content-Type" content"text/html; charsetutf-8" /><title>电子时钟</title><style>.a{position:absolute;-webkit-transform:rotate(0deg)}</style>…

《ECMAScript 6 入门教程》学习笔记 Ⅲ

Reflect 概述 Reflect对象与Proxy对象一样&#xff0c;也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。 &#xff08;1&#xff09; 将Object对象的一些明显属于语言内部的方法&#xff08;比如Object.defineProperty&#xff09;&#xff0c;放到…

数据结构 第二章 线性表 作业(已批改)

判断题 2-1-1 For a sequentially stored linear list of length N, the time complexities for query and insertion are O(1) and O(N), respectively. 译文&#xff1a;对于长度为N的顺序存储线性列表&#xff0c;查询和插入的时间复杂度分别为O(1)和O(N)。 T F 顺序存储…

当用户在输入框键入的用户名和密码为空时,在输入框右边提示进行提示,并阻止提交

1.为input输入框绑定onblur失去焦点事件&#xff0c;当失去焦点&#xff0c;调用检查函数 姓名:<input type"text" name"name" id"name" onblur"check()">2.设置隐藏提示信息 <span class"waring" id"nameC…

高淇python400第二季_python人工智能基础400集第二季

第1章 文件处理 3小时28分钟19节 1-1 file文件操作_操作系统底层关系_写入文件file文件操作_操作系统底层关系_写入文件 [18:15] 开始学习 1-2 编码知识_中文乱码问题解决编码知识_中文乱码问题解决 [18:47] 开始学习 1-3 关闭流要点1_try异常管理关闭流要点1_try异常管理 [11:…

数据结构 第四章 串、数组、广义表 作业(无批改)

串是一种特殊的线性表&#xff0c;其特殊性体现在&#xff08; &#xff09; A. 可以顺序存储 B. 数据元素是单个字符 C. 可以链式存储 D. 数据元素可以是多个字符 课本P87&#xff1a;串是一种特殊的线性表&#xff0c;其特殊性体现在数据元素是一个字符&#xff0c;也就是说…

stm32核心板可以点亮灯吗_电路设计与制作—基于STM32核心板的电路设计与制作流程...

电路设计与制作是每个电子相关专业&#xff0c;如电子信息工程、光电工程、自动化、电子科学与技术、生物医学工程、医疗器械工程等&#xff0c;必须掌握的技能。本篇文章将详细介绍基于STM32核心板的电路设计与制作流程&#xff0c;让大家先对电路设计与制作的过程有个总体的认…