ES6读书笔记(二)

时间:2022-05-31 18:05:23

前言

前段时间整理了ES6的读书笔记:《ES6读书笔记(一)》,现在为第二篇,本篇内容包括:

  • 一、数组扩展
  • 二、对象扩展
  • 三、函数扩展
  • 四、Set和Map数据结构
  • 五、Reflect

本文笔记也主要是根据阮一峰老师的《ECMAScript 6 入门》和平时的理解进行整理的,希望对你有所帮助,喜欢的就点个赞吧!

一、数组扩展

1. 扩展运算符

①复制数组:

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

②求最大值:

Math.max(...[14, 3, 77])
Math.max.apply(null, [14, 3, 77])

③合并数组:

let arr1 = ['a', 'b'];
let arr2 = ['c'];
let arr3 = ['d', 'e'];

// ES5:
arr1.concat(arr2, arr3);   // [ 'a', 'b', 'c', 'd', 'e' ] 为浅拷贝,slice、Object.assign()也为浅拷贝

// ES6:
[...arr1, ...arr2, ...arr3]    // [ 'a', 'b', 'c', 'd', 'e' ] 浅拷贝

// push:
Array.prototype.push.apply(arr1, arr2);   // 因为push参数不能为数组
arr1.push(...arr2);

④数组克隆:

// ES5:
const a1 = [1, 2];
const a2 = a1.concat();

// ES6:
const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;

⑤与解构赋值结合:

// ES5
a = list[0], rest = list.slice(1)
// ES6
[a, ...rest] = list

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

// 赋值时即在左边时只能放在参数最后一位
const [...butLast, last] = [1, 2, 3, 4, 5];  // 报错
const [first, ...middle, last] = [1, 2, 3, 4, 5];  // 报错

var arr1 = ['a', 'b'];
var a2 = [...arr1,2];   // 这样是没问题的,因为是扩展运算
console.log(a2)   // ['a', 'b', 2] 

⑥将字符串转为真正的数组:

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

//扩展运算符内部调用的是数据结构的Iterator接口,因此只有Iterator接口的对象才可以用扩展运算符转为真正的数组,Array.from也是如此,同时Array.from还支持转化类似数组的对象,即带有length属性的对象,可不含遍历器接口(Symbol.iterator):
let arrayLike = {
  '0': 'a',
  '1': 'b',
  '2': 'c',
  length: 3
};

let arr = [...arrayLike];  // TypeError: Cannot spread non-iterable object.
// 这个类似数组的对象没有Iterator接口,所以不能转化,可以使用Array.from()

Array.from({ length: 3 });  // [ undefined, undefined, undefined ]
Array.from({ length: 2 }, () => 'jack')   // ['jack', 'jack']

⑦扩展运算符后面是一个空数组,则不产生任何效果:

[...[], 1]  // [1]

2. Array上的方法:

①Array.from(数组,回调函数(val,index),this绑定)
第二个参数,类似于map方法,第三参数,this指向:

Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);

Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]

应用:
(1)可用于转化/拷贝数组:

//原方式:
Array.prototype.slice.call(arr);

let arr1 = ['a', 'b', 'c'];
let arr2 = Array.from(arr1); // ['a', 'b', 'c']

(2)处理空位:

var arrLike = { 
    length: 4, 
    2: "foo" 
}; 
Array.from( arrLike ); 
// [ undefined, undefined, "foo", undefined ]  // 和Array(3)产生空槽位不一样,这里是有undefined值的,会将空位转为undefined

②Array.of():用于将一组值,转换为数组:
与Array()不同,Array()的单参数会变为长度:

Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]

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

//模拟:
function ArrayOf(){
  return [].slice.call(arguments);
}

3. Array实例的方法:

①find((val, index, arr)=>{}, this) :用于找出【第一个】符合条件的【数组成员】,它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined,第二参数为this绑定。

[1, 4, -5, 10].find((n) => n < 0);
// -5  不是返回数组

[1, 5, 10, 15].find((value, index, arr) => value > 9); // 10

var a = [1,2,3,4,5];
(a.indexOf("2") != -1);   // false  indexOf是严格匹配===的,所以不会转化字符串

a.find(v => v == "2");    // 2  返回这个匹配的值,而不是返回布尔值
a.find(v => v == 7);      // undefined 

let person = {name: 'John', age: 20};
[10, 12, 26, 15].find((v) => { v > this.age;}, person);    // 26

②findIndex((val, index, arr)=>{}, this) :返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1,第二参数为this绑定。

find()和findIndex()都弥补了indexOf方法对NaN的不足:

[NaN].indexOf(NaN)   // -1
[NaN].findIndex(y => Object.is(NaN, y))   // 0

③copyWithin(target, start = 0, end = this.length)
target(必需):从该位置开始替换数据。如果为负值,表示倒数。
start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示倒数。
end(可选):到该位置 前 (这个位置前,所以不包含这个位置)停止读取数据,默认等于数组长度。如果为负值,表示倒数。

[1, 2, 3, 4, 5].copyWithin(0, 3)   // [4, 5, 3, 4, 5]  4、5替换1、2
[1, 2, 3, 4, 5].copyWithin(0, 2)   // [3, 4, 5, 4, 5]  3、4、5替换1、2、3
[1, 2, 3, 4, 5].copyWithin(0, 3, 4)  // [4, 2, 3, 4, 5]  4替换1

④entries(),keys()和values()——用于遍历数组:返回的是遍历器对象,可使用for of或者扩展运算符遍历出来:

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"

var a = [1,2,3]; 
[...a.values()];              // [1,2,3] 
[...a.keys()];                // [0,1,2] 
[...a.entries()];             // [ [0,1], [1,2], [2,3] ] 
[...a[Symbol.iterator]()];    // [1,2,3]

//也可手动next调用:
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方法返回一个布尔值(而find返回的是值),表示某个数组是否包含给定的值,与字符串的includes方法类似:

[1, 2, 3].includes(2)     // true
[1, 2, 3].includes(4)     // false
[1, 2, NaN].includes(NaN) // true  弥补了indexOf(使用===)对NaN的不足

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

[1, 2, 3].includes(3, 3);  // false
[1, 2, 3].includes(3, 4);  // false
[1, 2, 3].includes(3, -1); // true

⑥fill(填充项,起始位置,结束位置):使用给定值填充数组

var a = Array(4).fill(undefined); 
a;   // [undefined,undefined,undefined,undefined]

var a = [null, null, null, null].fill(42, 1, 3); 
a;   // [null,42,42,null]  不包含结束位置

//如果被赋值的是引用类型,是浅拷贝:
let arr = new Array(3).fill({name: "Mike"});
arr[0].name = "Ben";
arr    // [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]

⑦flat():用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响:

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

参数可设置拉平几层:

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

不管几层使用Infinity:

[1, [2, [3]]].flat(Infinity)   // [1, 2, 3]

会跳过空位:

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

flatMap():只能展开一层,相当于map方法,第二参为this

[2, 3, 4].flatMap((x) => [x, x * 2])     // [2, 4, 3, 6, 4, 8]

//相当于:
[2, 3, 4].map((x) => [x, x * 2 ]).flat()

[1, 2, 3, 4].flatMap(x => [[x * 2]]) // 只能展开一层 [[2], [4], [6], [8]]

4. 数组空位:空位不是undefined,是指没有值

var a = Array(3);
console.log(a)     // [empty × 3]
console.log(a[0])  // undefined 为空,找不到值,所以返回undefined

0 in [undefined, undefined, undefined] // true  说明0号位是有值的
0 in [, , ,]   // false

二、对象扩展

1. 对象属性

①简写:{x, y}

②属性名表达式:

let lastWord = 'last word';

const a = {
  'first word': 'hello',
  [lastWord]: 'world'
};

a['first word']   // "hello"
a[lastWord]    // "world"
a['last word']   // "world"

//表达式还可以用于定义方法名:
let obj = {
  ['h' + 'ello']() {
    return 'hi';
  }
};

obj.hello() // hi

//属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串[object Object]:
const keyA = {a: 1};
const keyB = {b: 2};

const myObject = {
  [keyA]: 'valueA',
  [keyB]: 'valueB'
};

myObject // Object {[object Object]: "valueB"}  'valueA'被覆盖了

③属性的可枚举性:设置为false,规避了for in操作,防止遍历到不可枚举属性。

Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable
// false

Object.getOwnPropertyDescriptor([], 'length').enumerable
// false

2.对象的解构赋值+扩展运算符:只能用在最后。

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

为浅拷贝:

let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.a.b = 2;
x.a.b // 2

扩展运算符:用于取出参数对象的所有可遍历属性(应该就只是自身可枚举属性),拷贝到当前对象之中。

let z = { a: 3, b: 4 };
let n = { ...z };
n  // { a: 3, b: 4 }

数组是特殊的对象,所以对象的扩展运算符也可以用于数组:

{...['a', 'b', 'c']}  // {0: "a", 1: "b", 2: "c"}

如果扩展运算符后面是字符串,它会自动转成一个类似数组的对象:

{...'hello'}   // {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}

如果扩展运算符后面是一个空对象,则没有任何效果:

{...{}, a: 1}  // { a: 1 }

如果扩展运算符后面不是对象,则会自动将其转为对象:

{...1}  // {} 等同于 {...Object(1)}

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

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

上面的例子只是拷贝了对象实例的属性,如果想完整克隆一个对象,还拷贝对象原型的属性,可以采用下面的写法:

// 写法一
const clone1 = {
  __proto__: Object.getPrototypeOf(obj),
  ...obj
};

// 写法二
const clone2 = Object.assign(
  Object.create(Object.getPrototypeOf(obj)),
  obj
);

扩展运算符可以用于合并两个对象:

let ab = {...a, ...b};
// 等同于
let ab = Object.assign({}, a, b);
)

3.super对象:指向当前对象的原型对象,super.foo等同于Object.getPrototypeOf(this).foo,只能用在对象的简写形式的方法之中

var o1 = { 
    foo() { 
        console.log("o1:foo"); 
    } 
}; 
var o2 = { 
    foo() { 
        super.foo();   // super相当于Object.getPrototypeOf(o2),指向o1,所以得到"o1:foo"的结果
        console.log("o2:foo"); 
    } 
}; 
Object.setPrototypeOf(o2, o1); 
o2.foo();      // o1:foo 
               // o2:foo

4. Object上新增的方法:

(1)Object.is():比较两个值是否相等,与严格比较运算符(===)的行为基本一致,不同之处只有两个:一是+0不等于-0,二是NaN等于自身:

+0 === -0       //true
NaN === NaN   // false

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

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

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

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会直接返回该参数:
const obj = {a: 1};
Object.assign(obj) === obj // true

// 如果该参数不是对象,则会先转成对象,然后返回:
typeof Object.assign(2) // "object"

// undefined和null无法转成对象,所以如果它们作为参数,就会报错:
Object.assign(undefined) // 报错
Object.assign(null) // 报错

// 如果非对象不是首参数,则会跳过(字符串会转为数字键对象),不会报错:
let obj = {a: 1};
Object.assign(obj, undefined) === obj   // true
Object.assign(obj, null) === obj   // true

// 其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果:
const v1 = 'abc';
const v2 = true;
const v3 = 10;

const obj = Object.assign({}, v1, v2, v3);
console.log(obj); // { "0": "a", "1": "b", "2": "c" }

// 只拷贝源对象的自身可枚举属性,包括属性名为 Symbol 值的属性:
Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' })
// { a: 'b', Symbol(c): 'd' }

// 且是浅拷贝:
const obj1 = {a: {b: 1}};
const obj2 = Object.assign({}, obj1);

obj1.a.b = 2;
obj2.a.b // 2

// 可以用来处理数组,但是会把数组视为对象:
Object.assign([1, 2, 3], [4, 5])   // [4, 5, 3]

上面代码中,Object.assign把数组视为属性名为 0、1、2 的对象,因此源数组的 0 号属性4覆盖了目标数组的 0 号属性1。

应用:
(1)为对象添加属性和方法:

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

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

(2)克隆或合并多个对象:

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

上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。
不过,采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承链,可以采用下面的代码。

function clone(origin) {
   let originProto = Object.getPrototypeOf(origin);
   return Object.assign(Object.create(originProto), origin);
}

(3)为属性指定默认值:

const DEFAULTS = {
   logLevel: 0,
   outputFormat: 'html'
   };

function processContent(options) {
   options = Object.assign({}, DEFAULTS, options);
   console.log(options);
   // ...
}

上面代码中,DEFAULTS对象是默认值,options对象是用户提供的参数。Object.assign方法将DEFAULTS和options合并成一个新对象,如果两者有同名属性,则option的属性值会覆盖DEFAULTS的属性值。

(3)Object.getOwnPropertyDescriptor():返回某个对象属性的描述对象。

const obj = {
  foo: 123,
  get bar() { return 'abc' }
};

Object.getOwnPropertyDescriptors(obj);
/*{ foo:
   { value: 123,
      writable: true,
      enumerable: true,
      configurable: true 
    },
   bar:
    { get: [Function: get bar],
      set: undefined,
      enumerable: true,
      configurable: true 
    } 
}
*/

继承:

const obj = Object.create(prot);
obj.foo = 123;
// 或者
const obj = Object.assign(
  Object.create(prot),
  {
    foo: 123,
  });

有了Object.getOwnPropertyDescriptors(),我们就有了另一种写法:

const obj = Object.create(
  prot,
  Object.getOwnPropertyDescriptors({
    foo: 123,
  }));

(4)Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替__proto__

①Object.setPrototypeOf(obj, prototype)

②Object.getPrototypeOf(obj)
如果参数不是对象,会被自动转为对象

(5)Object.keys()、Object.values()、Object.entries(),返回的都是数组,数组也有这些方法,但是返回的是遍历器对象。

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

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

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

const obj = {100: 'a', 2: 'b', 7: 'c'};
Object.values(obj);
// ["b", "c", "a"]
// 属性名为数值的属性,是按照数值大小,从小到大遍历的,因此返回的顺序是b、c、a

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

const obj = {foo: 'bar', baz: 42};
Object.entries(obj);
// [ ["foo", "bar"], ["baz", 42] ]

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

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

特别适合将Map结构转为对象:

const entries = new Map([
  ['foo', 'bar'],
  ['baz', 42]]);

Object.fromEntries(entries)
// { foo: "bar", baz: 42 }

三、函数扩展

1. 函数参数

(1)默认参数:

①不能重复声明:

function foo(x = 5) {
  let x = 1;   // error
  const x = 2; // error
}

②默认参数的位置应该是写在参数的末尾,这样才是自适应省略传参,否则如果默认参数放前面,如果不给默认传参,想给其它参数传参,则还是得向默认参数传递一个undefined,否则所传参数会覆盖默认参数,造成混淆。

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

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

(function(...args) {}).length     // 0

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

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

④产生作用域:
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的:

var x = 1;

function f(x, y = x) {   // 不会找到外部的x,因为参数作用域中第一个参数已经声明有了x,所以会y=x的x会指向它,而不会指向外部的x
  console.log(y);
}

f(2); // 2
f();  // undefined

//----------------------------------

let x = 1;

function f(y = x) {  // 会找到外部的x,因为参数中未声明x,所以会往上层作用域查找x
  let x = 2;
  console.log(y);
}

f(3); // 3
f();  // 1

⑤默认值表达式是惰性求值的,这意味着它们只在需要的时候运行——即在参数的值省略或者为undefined的时候:

function bar(val) { 
    console.log("bar called!"); 
    return y + val; 
} 
function foo(x = y + 3, z = bar(x)) {  // 形参作用域是在函数声明包裹的作用域,而不是在函数体
    console.log(x, z); 
} 
var y = 5; 
foo();                  // "bar called"
                        // 8 13
foo(10);                // "bar called"
                        // 10 15
y = 6; 
foo(undefined, 10);     // 9 10

//--------------------------------------------

var w = 1, z = 2; 
function foo(x = w + 1, y = x + 1, z = z + 1) { 
    console.log(x, y, z); 
} 
foo();    // ReferenceError 问题出在z+1中的z发现z是一个未初始化的参数变量,所以不会往上层作用域查找z

⑥undefined会触发默认值,null不会。

(2)参数的解构赋值:

function foo({x, y = 5}) {
  console.log(x, y);
}

foo({}) // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo()   // TypeError: Cannot read property 'x' of undefined 如果函数foo调用时没提供参数,变量x和y就不会生成,从而报错。通过提供函数参数的默认值,就可以避免这种情况:
function foo({x, y = 5} = {}) {
  console.log(x, y);
}

foo() // undefined 5

区别以下:

// 写法一
function m1({x = 0, y = 0} = {}) {
  return [x, y];
}

// 写法二
function m2({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}

// 函数没有参数的情况
m1() // [0, 0]
m2() // [0, 0]

// x 和 y 都有值的情况
m1({x: 3, y: 8}) // [3, 8]
m2({x: 3, y: 8}) // [3, 8]

// x 有值,y 无值的情况
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined]

// x 和 y 都无值的情况
m1({}) // [0, 0];
m2({}) // [undefined, undefined]

m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]

(3)参数使用...运算符:

①扩展运算符...之后不能再有其他参数:

// 报错
function f(a, ...b, c) {
  // ...
}

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

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

2. 箭头函数

①函数体内的this对象,就是定义时所在的对象(找离它最近的一个执行环境作为执行上下文,如对象方法中的箭头函数的this是指向window的,所以对象方法不要使用箭头函数),而不是使用时所在的对象。由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向:

function foo() {
  // setTimeout中的函数默认由全局环境执行,所以此时this指向window
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

var id = 21;

foo()  // id: 21
foo.call({id: 42});  // id: 42  call改变了this指向

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

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

function foo() {
  setTimeout(() => {
    console.log('args:', arguments);
  }, 100);
}

foo(2, 4, 6, 8)
// args: [2, 4, 6, 8]   得到的是外部函数的参数

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

不适用场合:

const cat = {
  lives: 9,
  jumps: () => {
    this.lives--;
    console.log(this.lives);
  }
}
cat.jumps();  // NaN

this指向:普通函数的 this 是动态的,所以要在运行时找拥有当前上下文的对象。

而箭头函数的 this 是静态的,也就是说,只需要看箭头函数在什么函数作用域下声明的,那么这个 this 就会绑定到这个函数的上下文中。即“穿透”箭头函数。

例子里的箭头函数并没有在哪个函数里声明,所以 thisfallback 到全局,全局的lives未声明,为undefined,运算后得到NaN。

(3)函数的name属性:

var f = function() {};
// ES5
f.name    // ""
// ES6
f.name    // "f"

const bar = function baz() {};
// ES5
bar.name   // "baz"
// ES6
bar.name   // "baz"

(4)双冒号运算符:

foo::bar;
// 等同于
bar.bind(foo);

foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);

(5)尾调用:指某个函数的最后一步是调用另一个函数。

function f(x){
  return g(x);
}

四、Set和Map数据结构

4.1 Set

1. Set类似于数组,但是成员的值都是唯一的,没有重复的值,它是构造函数,用于构造Set数据结构:

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  不会添加重复值

2. Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化:

const set = new Set([1, 2, 3, 4, 4]);  // 可为数组去重
set       // Set结构: {1, 2, 3, 4}
[...set]  // [1, 2, 3, 4]

数组或类数组去重:

Array.from(new Set(array))

也可去除字符串里的重复字符:

[...new Set('ababbc')].join('')
// "abc"

3. Set内部认为NaN是相等的,所以只能添加一个;两个对象也总是不相等的。

4. Array.from方法可以将 Set 结构转为数组:

const items = new Set([1, 2, 3, 4, 5]);
const array = Array.from(items);

5. Set实例属性:

// 构造函数,默认就是Set函数
Set.prototype.constructor

// 返回Set实例的成员总数
Set.prototype.size

6. Set实例方法:

四个操作方法:

  • ①add(value):添加某个值,返回 Set 结构本身。
  • ②delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • ③has(value):返回一个布尔值,表示该值是否为Set的成员。
  • ④clear():清除所有成员,没有返回值。
let s = new Set();

s.add(1).add(2).add(2);   // 注意2被加入了两次

s.size // 2

s.has(1) // true
s.has(2) // true
s.has(3) // false

s.delete(2);
s.has(2) // false

四个遍历方法:

  • ①keys():返回键名的遍历器
  • ②values():返回键值的遍历器
  • ③entries():返回键值对的遍历器
  • ④forEach():使用回调函数遍历每个成员

由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致:

let set = new Set(['red', 'green', 'blue']);

for (let item of set.keys()) {
  console.log(item);
}
// red
// green
// blue

for (let item of set.values()) {
  console.log(item);
}
// red
// green
// blue

for (let item of set.entries()) {
  console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]

Set 结构的实例默认可遍历,它的默认遍历器生成函数就是它的values方法:

Set.prototype[Symbol.iterator] === Set.prototype.values    // true
let set = new Set(['red', 'green', 'blue']);

for (let x of set) {
  console.log(x);
}
// red
// green
// blue

//方便实现并集、交集、差集:
let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);

// 并集
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}

// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}

// 差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}

4.2 WeakSet

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

①WeakSet 的成员只能是对象,而不能是其他类型的值:

const ws = new WeakSet();
ws.add(1)
// TypeError: Invalid value used in weak set
ws.add(Symbol())
// TypeError: invalid value used in weak set

②WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,且不可遍历。

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

const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]} 是数组的成员成为WeakSet 实例对象的成员,而不是a,所以成员必须是对象
const b = [3, 4];
const ws = new WeakSet(b);   // 成员不是对象,所以会报错
// Uncaught TypeError: Invalid value used in weak set(…)

三个方法:

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

没有size属性,无法遍历

4.3 Map

Map类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键:

const m = new Map();
const o = {p: 'Hello World'};

m.set(o, 'content')  // 将对象o作为键
m           // {{…} => "content"}
m.get(o)    // "content"

m.has(o)    // true
m.delete(o) // true
m.has(o)    // false

可接受数组作为参数,数组成员是一个个表示键值对的数组:

const map = new Map([
  ['name', '张三'],
  ['title', 'Author']
]);
map       // {"name" => "张三", "title" => "Author"}
map.size  // 2
map.has('name')  // true
map.get('name')  // "张三"
map.has('title') // true
map.get('title') // "Author"

不仅仅是数组,任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当作Map构造函数的参数。这就是说,Set和Map都可以用来生成新的 Map:

const set = new Set([
  ['foo', 1],
  ['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1

const m2 = new Map([['baz', 3]]);
const m3 = new Map(m2);
m3.get('baz') // 3

只有相同的简单类型的键才会被视为同一个键,否则如数组、对象等作为键时,不是同一个引用时就算是同名,也是不同的键:

const map = new Map();

map.set(['a'], 555);   // 数组对象键
map.get(['a']) // undefined 不是同一个地址

// ----------------------------------------------------

const map = new Map();

const k1 = ['a'];
const k2 = ['a'];

map
.set(k1, 111)
.set(k2, 222);

map.get(k1)  // 111 引用地址不同,所以是不同的键
map.get(k2)  // 222

属性方法:

  • ①size 属性
  • ②set(key, value)
  • ③get(key)
  • ④has(key)
  • ⑤delete(key)
  • ⑥clear()

三个遍历器生成函数和一个遍历方法:

  • keys():返回键名的遍历器。
  • values():返回键值的遍历器。
  • entries():返回所有成员的遍历器。
  • forEach():遍历 Map 的所有成员。
const map = new Map([
  ['F', 'no'],
  ['T', 'yes'],
]);

for (let key of map.keys()) {
  console.log(key);
}
// "F"
// "T"

for (let value of map.values()) {
  console.log(value);
}
// "no"
// "yes"

for (let item of map.entries()) {
  console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"

// 或者
for (let [key, value] of map.entries()) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

// 等同于使用map.entries()
for (let [key, value] of map) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"


map[Symbol.iterator] === map.entries   // true

可用扩展运算符转为数组:

const map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

[...map.keys()]
// [1, 2, 3]

[...map.values()]
// ['one', 'two', 'three']

[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]

[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]

数组转为Map:

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

4.4 WeakMap

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

const map = new WeakMap();
map.set(1, 2)
// TypeError: 1 is not an object!
map.set(Symbol(), 2)
// TypeError: Invalid value used as weak map key
map.set(null, 2)
// TypeError: Invalid value used as weak map key

②WeakMap的键名所指向的对象,不计入垃圾回收机制。
不能遍历且无size属性

五、Reflect

类似于proxy,reflect也是为了操作对象,reflect对象上可以拿到语言内部的方法:

// 旧写法
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}

// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}

// 旧写法
'assign' in Object // true

// 新写法
Reflect.has(Object, 'assign') // true

1. Reflect.get(target, name, receiver)

Reflect.get方法查找并返回target对象的name属性,如果没有该属性,则返回undefined:

var myObject = {
  foo: 1,
  bar: 2,
  get baz() {
    return this.foo + this.bar;
  },
};

var myReceiverObject = {
  foo: 4,
  bar: 4,
};

Reflect.get(myObject, 'baz', myReceiverObject)  // 8

2. Reflect.set(target, name, value, receiver)

Reflect.set方法设置target对象的name属性等于value:

var myObject = {
  foo: 1,
  set bar(value) {
    return this.foo = value;
  },
}

myObject.foo // 1

Reflect.set(myObject, 'foo', 2);
myObject.foo // 2

Reflect.set(myObject, 'bar', 3)
myObject.foo // 3

3. Reflect.has(obj, name)

Reflect.has方法对应name in obj里面的in运算符:

var myObject = {
  foo: 1,
};

// 旧写法
'foo' in myObject // true

// 新写法
Reflect.has(myObject, 'foo') // true
// 如果第一个参数不是对象,Reflect.has和in运算符都会报错。

4. Reflect.deleteProperty(obj, name)

Reflect.deleteProperty方法等同于delete obj[name],用于删除对象的属性:

const myObj = { foo: 'bar' };

// 旧写法
delete myObj.foo;

// 新写法
Reflect.deleteProperty(myObj, 'foo');

//该方法返回一个布尔值。如果删除成功,或者被删除的属性不存在,返回true;删除失败,被删除的属性依然存在,返回false。

5. Reflect.construct(target, args)

Reflect.construct方法等同于new target(...args),这提供了一种不使用new,来调用构造函数的方法:

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

// new 的写法
const instance = new Greeting('张三');

// Reflect.construct 的写法
const instance = Reflect.construct(Greeting, ['张三']);

6. Reflect.getPrototypeOf(obj)

Reflect.getPrototypeOf方法用于读取对象的__proto__属性,对应Object.getPrototypeOf(obj):

const myObj = new FancyThing();

// 旧写法
Object.getPrototypeOf(myObj) === FancyThing.prototype;

// 新写法
Reflect.getPrototypeOf(myObj) === FancyThing.prototype;

Reflect.getPrototypeOf和Object.getPrototypeOf的一个区别是,如果参数不是对象,Object.getPrototypeOf会将这个参数转为对象,然后再运行,而Reflect.getPrototypeOf会报错。

Object.getPrototypeOf(1)  // Number {[[PrimitiveValue]]: 0}
Reflect.getPrototypeOf(1) // 报错

7. Reflect.setPrototypeOf(obj, newProto)

Reflect.setPrototypeOf方法用于设置目标对象的原型(prototype),对应Object.setPrototypeOf(obj, newProto)方法。它返回一个布尔值,表示是否设置成功:

const myObj = {};

// 旧写法
Object.setPrototypeOf(myObj, Array.prototype);

// 新写法
Reflect.setPrototypeOf(myObj, Array.prototype);

myObj.length // 0

8. Reflect.apply(func, thisArg, args)

Reflect.apply方法等同于Function.prototype.apply.call(func, thisArg, args),用于绑定this对象后执行给定函数:

const ages = [11, 33, 12, 54, 18, 96];

// 旧写法
const youngest = Math.min.apply(Math, ages);
const oldest = Math.max.apply(Math, ages);
const type = Object.prototype.toString.call(youngest);

// 新写法
const youngest = Reflect.apply(Math.min, Math, ages);
const oldest = Reflect.apply(Math.max, Math, ages);
const type = Reflect.apply(Object.prototype.toString, youngest, []);

9. Reflect.defineProperty(target, propertyKey, attributes)

Reflect.defineProperty方法基本等同于Object.defineProperty,用来为对象定义属性。未来,后者会被逐渐废除,所以最好现在就开始使用Reflect.defineProperty代替它:

function MyDate() {
  /*…*/
}

// 旧写法
Object.defineProperty(MyDate, 'now', {
  value: () => Date.now()
});

// 新写法
Reflect.defineProperty(MyDate, 'now', {
  value: () => Date.now()
});

10. Reflect.getOwnPropertyDescriptor(target, propertyKey)

Reflect.getOwnPropertyDescriptor基本等同于Object.getOwnPropertyDescriptor,用于得到指定属性的描述对象,将来会替代掉后者:

var myObject = {};
Object.defineProperty(myObject, 'hidden', {
  value: true,
  enumerable: false,
});

// 旧写法
var theDescriptor = Object.getOwnPropertyDescriptor(myObject, 'hidden');

// 新写法
var theDescriptor = Reflect.getOwnPropertyDescriptor(myObject, 'hidden');

11. Reflect.isExtensible(target)

Reflect.isExtensible方法对应Object.isExtensible,返回一个布尔值,表示当前对象是否可扩展:

const myObject = {};

// 旧写法
Object.isExtensible(myObject) // true

// 新写法
Reflect.isExtensible(myObject) // true

12. Reflect.preventExtensions(target)

Reflect.preventExtensions对应Object.preventExtensions方法,用于让一个对象变为不可扩展。它返回一个布尔值,表示是否操作成功:

var myObject = {};

// 旧写法
Object.preventExtensions(myObject)  // Object {}

// 新写法
Reflect.preventExtensions(myObject) // true

13. Reflect.ownKeys(target)

Reflect.ownKeys方法用于返回对象的所有属性,基本等同于Object.getOwnPropertyNames与Object.getOwnPropertySymbols之和:

var myObject = {
  foo: 1,
  bar: 2,
  [Symbol.for('baz')]: 3,
  [Symbol.for('bing')]: 4,
};

// 旧写法
Object.getOwnPropertyNames(myObject)
// ['foo', 'bar']

Object.getOwnPropertySymbols(myObject)
//[Symbol(baz), Symbol(bing)]

// 新写法
Reflect.ownKeys(myObject)
// ['foo', 'bar', Symbol(baz), Symbol(bing)]

以上静态方法,如果传入的不是对象,基本都会报错

最后

好了,本篇就到这里,主要都是摘抄常用的知识点和备注自己的理解,希望对你有所帮助,后面会持续更新,也感谢你能看到这里!

GitHub传送门
博客园传送门
ES6读书笔记(二)