es6入门(二)

一、数组扩展

1.1 扩展运算符

1.1.1 扩展运算符的基本用法

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

扩展运算符主要在函数调用时候使用。

function add(x, y) {
  return x + y;
}

const numbers = [4, 38];
add(...numbers) // 42

扩展运算符与正常的函数参数可以结合使用,非常灵活

function f(v, w, x, y, z) { }
const args = [0, 1];
f(-1, ...args, 2, ...[3]);

扩展运算符后面还可以放置表达式。

const arr = [
  ...(x > 0 ? ['a'] : []),
  'b',
];

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

注意,只有函数调用时,扩展运算符才可以放在圆括号中,否则会报错。

(...[1, 2]) // 报错

console.log((...[1, 2])) // 报错

console.log(...[1, 2]) // 1 2
  • 由于扩展运算符可以展开数组,所以不再需要apply方法,将数组转为函数的参数了。
// ES5 的写法
function f(x, y, z) {
  // ...
}
var args = [0, 1, 2];
f.apply(null, args);

// ES6的写法
function f(x, y, z) {
  // ...
}
let args = [0, 1, 2];
f(...args);
  • 在Math.max函数中使用扩展运算符。
// ES5 的写法
Math.max.apply(null, [14, 3, 77])

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

// 等同于
Math.max(14, 3, 77);
  • 在数组push函数中使用扩展运算符。
// 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.1.2 扩展运算符的实际应用

  • 使用扩展运算符克隆数组。
const a1 = [1, 2];
const a2 = a1;

上面代码中,a2并不是a1的克隆,而是指向同一份数据的另一个指针。修改a2,会直接导致a1的变化。

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

上面的两种写法,a2都是a1的克隆。因此,修改a2就不会对a1产生影响。

  • 合并数组
const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];

// ES5 的合并数组
arr1.concat(arr2, arr3);

// ES6 的合并数组
[...arr1, ...arr2, ...arr3]

不过,这两种方法都是浅拷贝。因此,如果修改了原数组的成员,会同步反映到新数组。

  • 与解构赋值结合

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

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

first // 1
rest  // [2, 3, 4, 5]

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

const [...first, last] = [1, 2, 3, 4, 5]; // 报错

const [first, ...middle, last] = [1, 2, 3, 4, 5]; // 报错
  • 把字符串转换成数组
[...'hello'] // [ "h", "e", "l", "l", "o" ]

上面的写法,有一个重要的好处,那就是能够正确识别四个字节的 Unicode 字符。

'x\uD83D\uDE80y'.length // 4
[...'x\uD83D\uDE80y'].length // 3

上面代码的第一种写法,JavaScript 会将四个字节的 Unicode 字符,识别为 2 个字符,采用扩展运算符就没有这个问题。因此,正确返回字符串长度的函数,可以像下面这样写。

function length(str) {
  return [...str].length;
}

length('x\uD83D\uDE80y') // 3
  • 将实现了Iterator接口的对象转换成数组

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

let nodeList = document.querySelectorAll('div');
let arr = [...nodeList];

上面代码中,querySelectorAll方法返回的是一个nodeList对象。它不是数组,而是一个类似数组的对象。这时,扩展运算符可以将其转为真正的数组,原因就在于nodeList对象实现了 Iterator 。

1.2 Array.from()

Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象。

类似数组的对象包括:上面通过querySelectorAll方法返回的nodeList对象,方法中的arguments对象等等。

// 将nodeList转换成数组
let arr = Array.from(nodeList);

// arguments对象
function foo() {
  var args = Array.from(arguments);
}

下面是一个类似数组的对象,Array.from将它转为真正的数组。

let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
};

// ES5的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']

// ES6的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

另外,只要部署了Iterator接口的对象,也可以使用Array.from转换成成数组。

// 将字符串转换成数组
Array.from('hello') // ['h', 'e', 'l', 'l', 'o']

// 将实现了Iterator接口的对象转换成数组
let namesSet = new Set(['a', 'b'])
Array.from(namesSet) // ['a', 'b']

如果参数是一个真正的数组,Array.from会返回一个一模一样的新数组。

// 返回一个新的数组[1, 2, 3]
Array.from([1, 2, 3])

扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个接口,就无法转换。Array.from方法还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有length属性。因此,任何有length属性的对象,都可以通过Array.from方法转为数组,而此时扩展运算符就无法转换。

// 返回一个长度为3的空数组
Array.from({ length: 3 });  // [ undefined, undefined, undefined ]

上面代码中,Array.from返回了一个具有三个成员的数组,每个位置的值都是undefined。扩展运算符转换不了这个对象。

Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。

Array.from(arrayLike, x => x * x);

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

1.3 Array.of()

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

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

上面代码中,Array方法没有参数、一个参数、三个参数时,返回结果都不一样。只有当参数个数不少于 2 个时,Array()才会返回由参数组成的新数组。参数个数只有一个时,实际上是指定数组的长度。

Array.of基本上可以用来替代Array()或new Array(),并且不存在由于参数不同而导致的重载。它的行为非常统一。

Array.of() // []
Array.of(undefined) // [undefined]
Array.of(1) // [1]
Array.of(1, 2) // [1, 2]

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

1.4 copyWithin()

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

语法格式:

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

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

例如:

// 将3号位复制到0号位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4)  // [4, 2, 3, 4, 5]

// -2相当于3号位,-1相当于4号位
[1, 2, 3, 4, 5].copyWithin(0, -2, -1)  // [4, 2, 3, 4, 5]

// 将3号位复制到0号位
[].copyWithin.call({length: 5, 3: 1}, 0, 3)  // {0: 1, 3: 1, length: 5}

// 将2号位到数组结束,复制到0号位
let i32a = new Int32Array([1, 2, 3, 4, 5]);
i32a.copyWithin(0, 2);  // Int32Array [3, 4, 5, 4, 5]

1.5 find()和findIndex()

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

[1, 4, -5, 10].find((n) => n < 0) // -5

上面代码找出数组中第一个小于 0 的成员。

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

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

数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。

同时,这两个方法都可以接受第二个参数,用来绑定回调函数的this对象。

// 定义回调函数
function f(v){
  return v > this.age;
}

// 绑定this的对象
let person = {name: 'John', age: 20};

// 查找数组,返回大于person.age的数组元素
[10, 12, 26, 15].find(f, person);    // 26

1.6 fill()

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

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

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

上面代码表明,fill方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。

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

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

上面代码表示,fill方法从 1 号位开始,向原数组填充 7,到 2 号位之前结束。

如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。

let arr = new Array(3).fill({name: "Mike"});
arr[0].name = "Ben";
arr // [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]

上面代码修改了位置为0的数组元素的name属性,但是其他元素的name属性也被修改了。

1.7 entries()、keys()和values()

ES6提供了这三个方法用于遍历数组,它们返回一个遍历器对象,可以通过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"

如果不使用for…of循环,可以手动调用遍历器对象的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']

1.8 includes()

判断某个数组是否包含给定的值,如果包含返回true,否则返回false。

没有该方法之前,我们通常使用数组的indexOf方法,检查是否包含某个值。

if (arr.indexOf(el) !== -1) {
  // ...
}

indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判。例如:

[NaN].indexOf(NaN)  // -1

includes使用的是不一样的判断算法,就没有这个问题。

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

该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置。

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

二、对象扩展

对象(object)是 JavaScript 最重要的数据结构。ES6 对它进行了重大升级。

2.1 属性简洁表示法

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

const foo = 'bar';
const baz = {foo}; // 相当于{foo: "bar"}

// 等同于
const baz = {foo: foo};

上面代码中,变量foo直接写在大括号里面。这时,属性名就是变量名, 属性值就是变量值。

除了属性简写,方法也可以简写。

const o = {
  foo() {
    return "Hello!";
  }
};

// 等同于
const o = {
  foo: function() {
    return "Hello!";
  }
};

示例:

let birth = '2000/01/01';

const Person = {

  name: '张三',

  //等同于birth: birth
  birth,

  // 等同于hello: function ()...
  hello() { console.log('我的名字是', this.name); }

};

2.2 属性名表达式

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

// 方法一
obj.foo = true;

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

方法二是用表达式作为属性名,这时要将表达式放在方括号之内。

ES6 还允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内。

let propKey = 'foo';

let obj = {
  [propKey]: true,
  ['a' + 'bc']: 123
};

表达式还可以用于定义方法名。

let obj = {
  ['h' + 'ello']() {
    return 'hi';
  }
};

console.log(obj.hello()) // hi

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

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

注意,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串[object Object],这一点要特别小心。

const o1 = {a: 1};
const o2 = {b: 2};

const myObject = {
  [o1]: 'valueA',
  [o2]: 'valueB'
};

myObject // Object {[object Object]: "valueB"}

上面代码中,[o1]和[o2]得到的都是[object Object],所以[o2]会把[o1]覆盖掉。因此,myObject最后只有一个[object Object]属性。

2.3 super关键字

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

const proto = {
  foo: 'hello'
};

const obj = {
  foo: 'world',
  find() {
    return super.foo;
  }
};

Object.setPrototypeOf(obj, proto);
obj.find() // "hello"

上面代码中,对象obj.find()方法之中,通过super.foo引用了原型对象proto的foo属性。

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

// 报错
const obj = {
  foo: super.foo
}

// 报错
const obj = {
  foo: () => super.foo
}

// 报错
const obj = {
  foo: function () {
    return super.foo
  }
}

上面代码对于 JavaScript 引擎来说,这里的super都没有用在对象的方法之中。第一种写法是super用在属性里面,第二种和第三种写法是super用在一个函数里面,然后赋值给foo属性。目前,只有对象方法的简写法可以让 JavaScript 引擎确认,定义的是对象的方法。

JavaScript 引擎内部,super.foo等同于Object.getPrototypeOf(this).foo或Object.getPrototypeOf(this).foo.call(this)。因此,下面代码的super.foo指向原型对象proto的foo方法,但是绑定的this却还是当前对象obj,因此输出的就是world。

const proto = {
  x: 'hello',
  foo() {
    console.log(this.x);
  },
};

const obj = {
  x: 'world',
  foo() {
    super.foo();
  }
}

Object.setPrototypeOf(obj, proto);

obj.foo() // "world"

2.4 对象的扩展运算符

2.4.1 解构赋值

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

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

上面代码中,变量z是解构赋值所在的对象。它获取等号右边的所有尚未读取的键(a和b),将它们连同值一起拷贝过来。

由于解构赋值要求等号右边是一个对象,所以如果等号右边是undefined或null,就会报错,因为它们无法转为对象。

let { ...z } = null; // 运行时错误
let { ...z } = undefined; // 运行时错误

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

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

注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。

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

上面代码中,x是解构赋值所在的对象,拷贝了对象obj的a属性。a属性引用了一个对象,修改这个对象的值,会影响到解构赋值对它的引用。

另外,扩展运算符的解构赋值,不能复制继承自原型对象的属性。

let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
console.log('o2.b = ', o2.b) // 2
console.log('o2.a = ', o2.a) // 1

let { ...o3 } = o2;
console.log('o3.b = ', o3.b) // 2
console.log('o3.a = ', o3.a) // undefined

上面代码中,对象o3复制了o2,但是只复制了o2自身的属性,没有复制它的原型对象o1的属性。

// 指定原型对象创建对象o
const o = Object.create({ x: 1, y: 2 });
o.z = 3;
let { x, ...newObj } = o;

console.log(x) // 1
console.log(newObj.y) // undefined
console.log(newObj.z) // 3

上面代码中,变量x是单纯的解构赋值,所以可以读取对象o继承的属性;变量newObj.y和newObj.z是扩展运算符的解构赋值,只能读取对象o自身的属性,所以变量z可以赋值成功,变量y取不到值。

ES6 还规定,变量声明语句之中,如果使用解构赋值,扩展运算符后面必须是一个变量名,而不能是一个解构赋值表达式,所以上面代码引入了中间变量newObj,如果写成下面这样会报错。

let { x, ...newObj } = o;  // 正确
let { x, ...{ y, z } } = o; // 报错

下面是错误的信息:
在这里插入图片描述

2.4.2 扩展运算符

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

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

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

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

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

let foo = {...{}, a: 1}
foo
// { a: 1 }

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

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

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

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

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

上面代码中,扩展运算符后面不是对象,因此会自动转为Object{xx}。由于该对象没有自身属性,所以返回一个空对象。

但是,如果扩展运算符后面是字符串,它会自动转成一个类似数组的对象,因此返回的不是空对象。

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

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

let ab = {a: 10, b: 20}
let xy = {x: 30, y: 40}
let abxy = {...ab, ...xy};
abxy 
// {a: 10, b: 20, x: 30, y: 40}

如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉。

let x = {a: 10, b: 20}
let y = {...x, a: 30, c: 40};
y
// {a: 30, b: 20, c: 40}

如果把自定义属性放在扩展运算符前面,就变成了设置新对象的默认属性值。

let y = {a: 30, c: 40, ...x};
y
// {a: 10, b: 20, c: 40}

与数组的扩展运算符一样,对象的扩展运算符后面可以跟表达式。

const obj = {
  ...(x > 1 ? {a: 1} : {}),
  b: 2,
};

三、对象新增方法

3.1 Object.is()

作用:用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。

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

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

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

3.2 Object.assign()

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

const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
// 将source1和source2的所有属性复制到对象target中
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

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

如果只有一个参数,Object.assign会直接返回该参数。

const o1 = {a: 1};
const o2 = Object.assign(obj)
console.log(o1 == o2) // true

如果该参数不是对象,则会先转成对象,然后返回。

typeof Object.assign(2) // "object"

由于undefined和null无法转成对象,所以如果它们作为参数,就会报错。

Object.assign(undefined) // 报错
Object.assign(null) // 报错

如果非对象参数出现在源对象的位置(即非首参数),那么处理规则有所不同。首先,这些参数都会转成对象,如果无法转成对象,就会跳过。这意味着,下面代码不会报错。

let obj = {a: 1};
Object.assign(obj, undefined) // 运行正常
Object.assign(obj, null) // 运行正常

其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生任何效果。

const obj = Object.assign({}, 'abc', 10, true);
console.log(obj); // { "0": "a", "1": "b", "2": "c" }

上面的代码的10和true无法转换成对象,因为不是首参数,所以不会报错,但是也不会有任何效果。

另外一个值得我们注意的地方,Object.assign拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性。

注意事项

(1)浅拷贝
Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。

const obj1 = {a: {b: 1}};
const obj2 = Object.assign({}, obj1);

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

(2)同名属性的替换
对于这种嵌套的对象,一旦遇到同名属性,Object.assign的处理方法是替换,而不是添加。

const target = { a: { b: 'c', d: 'e' } }
const source = { a: { b: 'hello' } }
Object.assign(target, source)
// { a: { b: 'hello' } }

(3)数组处理
Object.assign可以用来处理数组,但是会把数组视为对象。

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

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

(4)取值函数的处理
Object.assign只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。

const source = {
  get foo() { return 1 }
};
const target = {};

Object.assign(target, source)
// { foo: 1 }

上面代码中,source对象的foo属性是一个取值函数,Object.assign不会复制这个取值函数,只会拿到值以后,将这个值复制过去。

Object.assign()的实际用途

(1)为对象添加属性

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

(2)为对象添加方法

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

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

(3)克隆对象

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

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

不过,采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承链,可以采用下面的代码。

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

(4)合并多个对象

Object.assign(target, ...sources);

如果希望合并后返回一个新对象,可以改写上面函数,对一个空对象合并。

Object.assign({}, ...sources)

(5)为属性指定默认值

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

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

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

注意,由于存在浅拷贝的问题,DEFAULTS对象和options对象的所有属性的值,最好都是简单类型,不要指向另一个对象。

const DEFAULTS = {
  url: {
    host: 'example.com',
    port: 7070
  },
};

const newObj = Object.assign(DEFAULTS, { url: {port: 8000}})
url.port // 8000
url.host // 不存在

上面代码的原意是将url.port改成 8000,url.host保持不变。实际结果却是options.url覆盖掉DEFAULTS.url,所以url.host就不存在了。

3.3 Object.getOwnPropertyDescriptors()

ES5 的Object.getOwnPropertyDescriptor()方法会返回某个对象属性的描述对象(descriptor)。ES2017 引入了Object.getOwnPropertyDescriptors()方法,返回指定对象所有自身属性(非继承属性)的描述对象。

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

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

上面代码中,Object.getOwnPropertyDescriptors()方法返回一个对象,所有原对象的属性名都是该对象的属性名,对应的属性值就是该属性的描述对象。

该方法的引入目的,主要是为了解决Object.assign()无法正确拷贝get属性和set属性的问题。

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

const target = {}

// 将obj所有属性合并到target对象中
Object.assign(target, obj)

console.log(target)

运行结果:
在这里插入图片描述
上面代码中,source对象的bar属性的值是一个get函数,Object.assign方法将这个属性拷贝给target对象,结果该属性的值变成了abc。这是因为Object.assign方法总是拷贝一个属性的值,而不会拷贝它背后的赋值方法或取值方法。

这时,Object.getOwnPropertyDescriptors()方法配合Object.defineProperties()方法,就可以实现正确拷贝。

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

const target = {}

// 给target对象定义属性
Object.defineProperties(target, 
	Object.getOwnPropertyDescriptors(obj));

console.log(target)

Object.getOwnPropertyDescriptors()方法的另一个用处,是配合Object.create()方法,将对象属性克隆到一个新对象。这属于浅拷贝。

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

// 克隆对象
const clone = Object.create(
	Object.getPrototypeOf(obj), 
	Object.getOwnPropertyDescriptors(obj));
	
obj.foo.aa = 999
console.log(clone.foo.aa) // 999

从输出结果看出,obj.foo和clone.foo其实是同一对象的引用。

另外,Object.getOwnPropertyDescriptors()方法可以实现一个对象继承另一个对象。以前,继承另一个对象,常常写成下面这样。

const prot = {
  get bar() { return 'abc' }
};

// 继承obj
const obj = {
  __proto__: prot,
  foo: 123,
};

console.log(childObj.bar)

如果去除__proto__,上面代码就要改成下面这样。

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}) // 添加到新对象的可枚举属性
);

3.4 proto,Object.setPrototypeOf(),Object.getPrototypeOf()

(1)__proto__属性

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

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

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

虽然可以使用__proto__属性设置原型对象,但是es6标准明确规定,除了浏览器必须部署该属性以外,在代码中最好认为这个属性是不存在的。因此,无论从语义的角度,还是从兼容性的角度,都不要使用这个属性,而是使用Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。

(2)Object.setPrototypeOf()

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

// 格式
Object.setPrototypeOf(object, prototype)

该方法等同于:

function setPrototypeOf(obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

例如:

let proto = {};
let obj = { x: 10 };
Object.setPrototypeOf(obj, proto);

proto.y = 20;
proto.z = 40;

console.log(obj.x) // 10
console.log(obj.y) // 20
console.log(obj.z) // 40

上面代码将proto对象设为obj对象的原型,所以从obj对象可以读取proto对象的属性。

如果第一个参数不是对象,会自动转为对象。但是如果第一个参数是undefined或null,就会报错。

Object.setPrototypeOf(undefined, {})
// TypeError: Object.setPrototypeOf called on null or undefined

Object.setPrototypeOf(null, {})
// TypeError: Object.setPrototypeOf called on null or undefined

(3)Object.getPrototypeOf()

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

// 格式
Object.getPrototypeOf(obj);

例如:

function Rectangle() {
  // ...
}
const rec = new Rectangle();

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

如果参数不是对象,会被自动转为对象。

// 等同于 Object.getPrototypeOf(Number(1))
Object.getPrototypeOf(1)
// Number {[[PrimitiveValue]]: 0}

// 等同于 Object.getPrototypeOf(String('foo'))
Object.getPrototypeOf('foo')
// String {length: 0, [[PrimitiveValue]]: ""}

// 等同于 Object.getPrototypeOf(Boolean(true))
Object.getPrototypeOf(true)
// Boolean {[[PrimitiveValue]]: false}

Object.getPrototypeOf(1) === Number.prototype // true
Object.getPrototypeOf('foo') === String.prototype // true
Object.getPrototypeOf(true) === Boolean.prototype // true

如果参数是undefined或null,它们无法转为对象,所以会报错。

Object.getPrototypeOf(null)
// TypeError: Cannot convert undefined or null to object

Object.getPrototypeOf(undefined)
// TypeError: Cannot convert undefined or null to object

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

(1)Object.keys()

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

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

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

let {keys, values, entries} = Object;
let obj = { a: 1, b: 2, c: 3 };

for (let key of keys(obj)) {
  console.log(key); // 'a', 'b', 'c'
}

for (let value of values(obj)) {
  console.log(value); // 1, 2, 3
}

for (let [key, value] of entries(obj)) {
  console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]
}

(2)Object.values()

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

const obj = { 100: 'a', 2: 'b', 7: 'c' };
Object.values(obj)
// ["b", "c", "a"]

上面代码中,属性名为数值的属性,是按照数值大小,从小到大遍历的,因此返回的顺序是b、c、a。

Object.values只返回对象自身的可遍历属性。

const obj = Object.create({}, {p: {value: 42}});
Object.values(obj) // []

上面代码中,Object.create方法的第二个参数添加的对象属性(属性p),如果不显式声明,默认是不可遍历的,因为p的属性描述对象的enumerable默认是false,Object.values不会返回这个属性。只要把enumerable改成true,Object.values就会返回属性p的值。

var prot = {
	p: {
		value: 42,
		enumerable: true,
	},
}
const obj = Object.create({}, prot);
Object.values(obj) // [42]

如果Object.values方法的参数是一个字符串,会返回各个字符组成的一个数组。

Object.values('foo')
// ['f', 'o', 'o']

上面代码中,字符串会先转成一个类似数组的对象。字符串的每个字符,就是该对象的一个属性。因此,Object.values返回每个属性的键值,就是各个字符组成的一个数组。

如果参数不是对象,Object.values会先将其转为对象。由于数值和布尔值的包装对象,都不会为实例添加非继承的属性。所以,Object.values会返回空数组。

Object.values(42) // []
Object.values(true) // []

(3)Object.entries()

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

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

除了返回值不一样,该方法的行为与Object.values基本一致。

Object.entries的基本用途是遍历对象的属性。

let obj = { one: 1, two: 2 };
for (let [k, v] of Object.entries(obj)) {
  console.log(
    `${JSON.stringify(k)}: ${JSON.stringify(v)}`
  );
}

Object.entries方法的另一个用处是,将对象转为真正的Map结构。

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

3.6 Object.fromEntries()

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

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

该方法的主要目的,是将键值对的数据结构还原为对象,因此特别适合将 Map 结构转为对象。

const map = new Map().set('foo', true).set('bar', false);
Object.fromEntries(map)

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

Object.fromEntries(new URLSearchParams('foo=bar&baz=qux'))
// { foo: "bar", baz: "qux" }
发布了111 篇原创文章 · 获赞 41 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/zhongliwen1981/article/details/102386176