对象和数组的扩展用法(前端进阶1.5)

582 阅读11分钟

一、对象的扩展

1.1 属性的简洁表达

const foo = 'bar';
const baz = {foo};
console.log(baz)//{foo: "bar"}

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

简写法:打印对象

let user = {
  name: 'test'
};

let foo = {
  bar: 'baz'
};

console.log(user, foo)
// {name: "test"} {bar: "baz"}
console.log({user, foo})
// {user: {name: "test"}, foo: {bar: "baz"}}

console.log直接输出 userfoo 两个对象时,就是两组键值对,可能会混淆。把它们放在大括号里面输出,就变成了对象的简洁表示法,每组键值对前面会打印对象名,这样就比较清晰了。

1.2 属性名表达式

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

  • 一是直接用标识符作为属性名;
  • 二是用表达式作为属性名,这时要将表达式放在方括号之内。

那么什么是标识符呢?

在JavaScript中,标识符只能包含字母或数字或下划线(“_”)或美元符号(“$”),且不能以数字开头。标识符与字符串不同之处在于字符串是数据,而标识符是代码的一部分。在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';
  }
};

obj.hello() // hi
const keyA = {a: 1};
const keyB = {b: 2};

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

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

分析: 若属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串[object Object];

所以,如果在同一个对象里有多个为对象的属性名,如[keyA][keyB]得到的都是[object Object][keyB]会把[keyA]覆盖掉,而最后只有一个[object Object]属性。

1.3 方法的name属性

函数的name属性返回函数名

var p ={
  sayHi:function(){
    console.log("hi");
  }
}
console.log(p.sayHi.name) // sayHi

1.4 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 proto = {
  x: 'hello',
  foo() {
    console.log(this.x);
  },
};

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

Object.setPrototypeOf(obj, proto);

obj.foo() // "world"

上面代码中,super.foo指向原型对象proto的foo方法,但是绑定的this却还是当前对象obj,因此输出的就是world。

1.5 Object.is()

Object.is() 方法判断两个值是否为同一个值。

语法:

Object.is(value1, value2);

参数: value1被比较的第一个值。value2被比较的第二个值。

返回值:一个 Boolean 类型标示两个参数是否是同一个值。

描述:

Object.is() 方法判断两个值是否为同一个值。如果满足以下条件则两个值相等:

  • 都是 undefined
  • 都是 null
  • 都是 true 或 false
  • 都是相同长度的字符串且相同字符按相同顺序排列
  • 都是相同对象(意味着每个对象有同一个引用)
  • 都是数字且
    • 都是 +0
    • 都是 -0
    • 都是 NaN
    • 或都是非零而且非 NaN 且为同一个值 与 == 运算不同。 == 运算符在判断相等前对两边的变量(如果它们不是同一类型) 进行强制转换 (这种行为的结果会将 "" == false 判断为 true), 而 Object.is不会强制转换两边的值。

=== 运算也不相同。 === 运算符 (也包括 == 运算符) 将数字 -0 和 +0 视为相等 ,而将Number.NaN 与NaN视为不相等。

Polyfill

if (!Object.is) {
  Object.is = function(x, y) {
    // SameValue algorithm
    if (x === y) { // Steps 1-5, 7-10
      // Steps 6.b-6.e: +0 != -0
      return x !== 0 || 1 / x === 1 / y;
    } else {
      // Step 6.a: NaN == NaN
      return x !== x && y !== y;
    }
  };
}

例子

Object.is('foo', 'foo');     // true
Object.is(window, window);   // true

Object.is('foo', 'bar');     // false
Object.is([], []);           // false

var foo = { a: 1 };
var bar = { a: 1 };
Object.is(foo, foo);         // true
Object.is(foo, bar);         // false

Object.is(null, null);       // true

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

1.6 Object.assign()

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。

const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };

const returnedTarget = Object.assign(target, source);

console.log(target);
// expected output: Object { a: 1, b: 4, c: 5 }

console.log(returnedTarget);
// expected output: Object { a: 1, b: 4, c: 5 }

语法:

Object.assign(target, ...sources)

参数:target 目标对象。sources 源对象。

返回值:目标对象。

描述:

如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后面的源对象的属性将类似地覆盖前面的源对象的属性。

Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象。该方法使用源对象的[[Get]]和目标对象的[[Set]],所以它会调用相关 gettersetter。因此,它分配属性,而不仅仅是复制或定义新的属性。如果合并源包含 getter,这可能使其不适合将新属性合并到原型中。为了将属性定义(包括其可枚举性)复制到原型,应使用Object.getOwnPropertyDescriptor()Object.defineProperty()

String 类型和 Symbol 类型的属性都会被拷贝。

在出现错误的情况下,例如,如果属性不可写,会引发 TypeError,如果在引发错误之前添加了任何属性,则可以更改 target 对象。

注意,Object.assign 不会在那些 source 对象值为 nullundefined 的时候抛出错误。 Polyfill

这个 polyfill 不支持 symbol 属性, 由于 ES5 中本来就不存在 symbols :

if (typeof Object.assign !== 'function') {
  // Must be writable: true, enumerable: false, configurable: true
  Object.defineProperty(Object, "assign", {
    value: function assign(target, varArgs) { // .length of function is 2
      'use strict';
      if (target === null || target === undefined) {
        throw new TypeError('Cannot convert undefined or null to object');
      }

      var to = Object(target);

      for (var index = 1; index < arguments.length; index++) {
        var nextSource = arguments[index];

        if (nextSource !== null && nextSource !== undefined) {
          for (var nextKey in nextSource) {
            // Avoid bugs when hasOwnProperty is shadowed
            if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
              to[nextKey] = nextSource[nextKey];
            }
          }
        }
      }
      return to;
    },
    writable: true,
    configurable: true
  });
}

例子

  • 复制一个对象
const obj = { a: 1 };
const copy = Object.assign({}, obj);
console.log(copy); // { a: 1 }
  • 深拷贝问题 针对深拷贝,需要使用其他办法,因为 Object.assign()拷贝的是(可枚举)属性值。

假如源值是一个对象的引用,它仅仅会复制其引用值。

const log = console.log;

function test() {
  'use strict';
  let obj1 = { a: 0 , b: { c: 0}};
  let obj2 = Object.assign({}, obj1);
  log(JSON.stringify(obj2));
  // { a: 0, b: { c: 0}}

  obj1.a = 1;
  log(JSON.stringify(obj1));
  // { a: 1, b: { c: 0}}
  log(JSON.stringify(obj2));
  // { a: 0, b: { c: 0}}

  obj2.a = 2;
  log(JSON.stringify(obj1));
  // { a: 1, b: { c: 0}}
  log(JSON.stringify(obj2));
  // { a: 2, b: { c: 0}}

  obj2.b.c = 3;
  log(JSON.stringify(obj1));
  // { a: 1, b: { c: 3}}
  log(JSON.stringify(obj2));
  // { a: 2, b: { c: 3}}

  // Deep Clone
  obj1 = { a: 0 , b: { c: 0}};
  let obj3 = JSON.parse(JSON.stringify(obj1));
  obj1.a = 4;
  obj1.b.c = 4;
  log(JSON.stringify(obj3));
  // { a: 0, b: { c: 0}}
}

test();
  • 合并对象
const o1 = { a: 1 };
const o2 = { b: 2 };
const o3 = { c: 3 };

const obj = Object.assign(o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }
console.log(o1);  // { a: 1, b: 2, c: 3 }, 注意目标对象自身也会改变。
  • 合并具有相同属性的对象
const o1 = { a: 1, b: 1, c: 1 };
const o2 = { b: 2, c: 2 };
const o3 = { c: 3 };

const obj = Object.assign({}, o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }

属性被后续参数中具有相同属性的其他对象覆盖。

  • 拷贝 symbol 类型的属性
const o1 = { a: 1 };
const o2 = { [Symbol('foo')]: 2 };

const obj = Object.assign({}, o1, o2);
console.log(obj); // { a : 1, [Symbol("foo")]: 2 } (cf. bug 1207182 on Firefox)
Object.getOwnPropertySymbols(obj); // [Symbol(foo)]
  • 继承属性和不可枚举属性是不能拷贝的
const obj = Object.create({foo: 1}, { // foo 是个继承属性。
    bar: {
        value: 2  // bar 是个不可枚举属性。
    },
    baz: {
        value: 3,
        enumerable: true  // baz 是个自身可枚举属性。
    }
});

console.log({obj})

const copy = Object.assign({}, obj);
console.log({copy}); // { baz: 3 }

  • 原始类型会被包装为对象
const v1 = "abc";
const v2 = true;
const v3 = 10;
const v4 = Symbol("foo")

const obj = Object.assign({}, v1, null, v2, undefined, v3, v4);
// 原始类型会被包装,null 和 undefined 会被忽略。
// 注意,只有字符串的包装对象才可能有自身可枚举属性。
console.log(obj); // { "0": "a", "1": "b", "2": "c" }
  • 异常会打断后续拷贝任务
const target = Object.defineProperty({}, "foo", {
    value: 1,
    writable: false
}); // target 的 foo 属性是个只读属性。

Object.assign(target, {bar: 2}, {foo2: 3, foo: 3, foo3: 3}, {baz: 4});
// TypeError: "foo" is read-only
// 注意这个异常是在拷贝第二个源对象的第二个属性时发生的。

console.log(target.bar);  // 2,说明第一个源对象拷贝成功了。
console.log(target.foo2); // 3,说明第二个源对象的第一个属性也拷贝成功了。
console.log(target.foo);  // 1,只读属性不能被覆盖,所以第二个源对象的第二个属性拷贝失败了。
console.log(target.foo3); // undefined,异常之后 assign 方法就退出了,第三个属性是不会被拷贝到的。
console.log(target.baz);  // undefined,第三个源对象更是不会被拷贝到的。
  • 拷贝访问器
const obj = {
  foo: 1,
  get bar() {
    return 2;
  }
};

let copy = Object.assign({}, obj);
console.log(copy); // { foo: 1, bar: 2 } copy.bar的值来自obj.bar的getter函数的返回值

// 下面这个函数会拷贝所有自有属性的属性描述符
function completeAssign(target, ...sources) {
  sources.forEach(source => {
    let descriptors = Object.keys(source).reduce((descriptors, key) => {
      descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
      return descriptors;
    }, {});

    // Object.assign 默认也会拷贝可枚举的Symbols
    Object.getOwnPropertySymbols(source).forEach(sym => {
      let descriptor = Object.getOwnPropertyDescriptor(source, sym);
      if (descriptor.enumerable) {
        descriptors[sym] = descriptor;
      }
    });
    Object.defineProperties(target, descriptors);
  });
  return target;
}

copy = completeAssign({}, obj);
console.log(copy);
// { foo:1, get bar() { return 2 } }

Polyfill

这个 polyfill 不支持 symbol 属性, 由于 ES5 中本来就不存在 symbols :

if (typeof Object.assign != 'function') {
  // Must be writable: true, enumerable: false, configurable: true
  Object.defineProperty(Object, "assign", {
    value: function assign(target, varArgs) { // .length of function is 2
      'use strict';
      if (target == null) { // TypeError if undefined or null
        throw new TypeError('Cannot convert undefined or null to object');
      }

      let to = Object(target);

      for (var index = 1; index < arguments.length; index++) {
        var nextSource = arguments[index];

        if (nextSource != null) { // Skip over if undefined or null
          for (let nextKey in nextSource) {
            // Avoid bugs when hasOwnProperty is shadowed
            if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
              to[nextKey] = nextSource[nextKey];
            }
          }
        }
      }
      return to;
    },
    writable: true,
    configurable: true
  });
}

1.7 属性的可枚举性

  • 什么是枚举?

    • 可枚举可以理解为是否可以被遍历被列举出来,可枚举性决定了这个属性能否被for…in查找遍历到。

    • js中基本包装类型的原型属性是不可枚举的(不可被 for…in… 遍历),比如Boolean,NumberString 三个的原型属性,或是 Boolean ,Number 值,都是不可枚举的,即是基本类型,也是引用类型。基本包装类型还可以像引用类型一样访问它自带的一些方法,但是不能像引用类型那样自定义方法。

    • 每个属性都有一个属性描述符(Descriptor)用以控制该属性的行为,可以使用Object.getOwnPropertyDescriptor()来获取对象的描述。 ES5中有以下3个方法会忽略enumerable为false的属性:

    for...in:只遍历对象自身的和继承的可枚举的属性
    Object.keys():返回对象自身的所有可枚举的属性的键名
    JSON.stringify():只串行化对象自身的可枚举的属性 ES6新增了一个操作,也会忽略enumerable为false的属性: Object.assign():忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。
    Reflect.enumerable():返回所有for...in会遍历到的属性(该方法在es7被废弃)

  • 判断属性是否可枚举的方法

    • for…in…
    var num = new Number();
    for(var pro in num) {
        console.log("num." + pro + " = " + num[pro]);
    }
    

    无法打印,number中的属性无法被枚举,所以 for…in…不起作用

    注意:

    并不是所有的属性都会在for-in循环中显示。数组的 length 属性和 constructor 属性就不会被显示。

    • Object.propertyIsEnumerable()

      1. propertyIsEnumerable() 方法返回一个布尔值,表示属性是否可以枚举
      2. 每个对象都有一个propertyIsEnumerable方法。该方法可以确定对象中的指定属性是否可以通过for…in循环枚举,但通过原型链继承的属性除外。如果对象不具有指定的属性,则此方法返回false。 语法: obj.propertyIsEnumerable(prop),prop:表示要测试的属性名称

      返回值:Boolean 类型

      //例一:
      var obj = {}
      obj.propertyIsEnumerable('a') //false
      
      //例二:
      var obj = {a1}
      obj.propertyIsEnumerable('a') //true
      

      简单来说,用户定义的属性都是可枚举的,而内置对象不可枚举。

      一种情况除外:当属性的原型是继承于其它对象原型时,这时用户定义的属性就是不可枚举的

1.8 属性的遍历

ES6有6种方法可以遍历属性:

  • for...in
  • Object.keys(obj)
  • Object.getOwnPropertyNames(obj)
  • Object.getOwnPropertySymbols(obj)
  • Reflect.ownKeys(obj)
  • Reflect.enumerable(obj)

1.9 ___proto__属性,Object.setPropertyOf(),Object.getPrototypeOf()

  • _proto__属性用来读取或设置对象的prototype对象
  • Object.setPropertyOf()用来设置属性的prototype对象
  • Object.getPropertyOf()用来获取属性的prototype对象 想了解更多对象用法,可以参考该文档

二、数组的扩展

2.1 Array.form()

Array.from()将类数组(array-like)对象与可迭代对象(包括set和map)转化为真正的数组并返回;

let arrayLike={
  "0":"a",
  "1":"b",
  "2":"c",
  length:3
}
ES5: 
var arr1 = [].slice.call(arrayLike);	//["a","b","c"]
ES6: 
var arr2 = Array.from(arrayLike);	//["a","b","c"]

Array.from()还接受第二个参数,第二个参数是一个方法,类似map一样对每个元素进行处理:

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

2.2 Array.of()

Array.of()用于将一组值转换为数组

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

2.3 数组实例的copyWithin()

copyWithin()方法用以将指定位置的成员复制到其他位置,覆盖目标位置的值,会修改原数组。

语法:Array.prototype.copyWithin(target,source=0,end=this.length):

  • target,必须,目标位置,从该位置开始替换数据
  • start,可选,从该位置开始读取
  • end,可选,到当前位置结束,默认等于数组的长度
[1,2,3,4,5].copyWithin(0,3);//原数组变为 [4, 5, 3, 4, 5] ,默认end为5
[1,2,3,4,5].copyWithin(0,3,4);// [4, 2, 3, 4, 5]

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

  • find()方法用来找出第一个符合条件的数组成员
  • findIndex()用来找出第一个符合条件的成员索引
[1,5,10,15].find(function(value,index,arr){
	return value>9;
});	//10

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

2.5 数组实例的fill()

使用给定的值填充数组,初始化数组时很方便

["a","b","c"].fill(7);	//[7,7,7]
new Array(3).fill(0);	//[0,0,0]

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

都用于遍历数组,都返回一个遍历器对象。可以使用for...of循环,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。

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

for(let values of ["a","b"].values(){
	console.log(values)
})	//"a","b"

for(let [index,value] of ["a","b"].entries(){
	console.log(index+" "+value);	
})	//"0 a","1 b"

2.7 数组实例的includes()方法

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

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

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

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

indexOf()方法有两个缺点:

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

2.8 数组的空位

数组的空位指的是数组的某一个位置没有任何值。

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

0 in [undefined, undefined, undefined] // true
0 in [, , ,] // false

ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位。

  • forEach(), filter(), reduce(), every() 和some()都会跳过空位。
  • map()会跳过空位,但会保留这个值
  • join()和toString()会将空位视为undefined,而undefined和null会被处理成空字符串。 ES6 则是明确将空位转为 undefined

Array.from()方法会将数组的空位,转为 undefined,也就是说,这个方法不会忽略空位。

let arrayLike={
  "0":"a",
  "2":"b",
  "3":"c",
  length:4
}
// ES5: 
var arr1 = [].slice.call(arrayLike);	//["a", empty, "b", "c"]
// ES6: 
var arr2 = Array.from(arrayLike);	//["a", undefined, "b", "c"]
Array.from(['a',,'b']) // [ "a", undefined, "b" ]

扩展运算符(...)也会将空位转为 undefined

[...['a',,'b']]// [ "a", undefined, "b" ]

copyWithin()会连空位一起拷贝

[,'a','b',,].copyWithin(2,0) // [,"a",,"a"]

fill()会将空位视为正常的数组位置

new Array(3).fill('a') // ["a","a","a"]

for...of循环也会遍历空位

let arr = [,,];
for (let i of arr) {
	console.log(1);// 1 // 1
}

上面代码中,数组arr有两个空位,for...of并没有忽略它们。如果改成map方法遍历,空位是会跳过的。

entries()keys()values()find()findIndex()会将空位处理成undefined。

// entries()
[...[,'a'].entries()] // [[0,undefined], [1,"a"]]

// keys()
[...[,'a'].keys()] // [0,1]

// values()
[...[,'a'].values()] // [undefined,"a"]

// find()
[,'a'].find(x => true) // undefined

// findIndex()
[,'a'].findIndex(x => true) // 0

由于空位的处理规则非常不统一,所以建议避免出现空位