ES6学习笔记(十)对象的扩展

本文深入探讨了ES6中对象的扩展特性,包括属性的新表示法、简便写法、属性名表达式、属性的遍历、super关键字的使用、扩展运算符及新增的方法等。通过实例解析了属性遍历的规则、super的正确使用场景以及如何利用扩展运算符和新增方法进行对象操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ES6对于对象的扩展,主要是在其属性的操作上提供了更多方便,属性的表示更为简洁方便,对属性更好地遍历,提供了几个新的方法。

属性的新表示法


属性的简便写法

如果在属性块外声明一个变量,将该变量放入属性块中,就能直接生成对应的属性名和属性值,属性名就是变量名,属性值就是该变量的值

let a=1;

foo={a};

foo//{a:1}

简便写法和普通的写法可以结合

let a=1;

foo={a,b:2}

//{a: 1, b: 2}

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

方法的简便写法

var obj = {
  class () {}
};

// 等同于
var obj = {
  'class': function() {}
};

生成器方法也能简写,如下代码

var obj={
    *fn(){}
}

对象的箭头函数方法不能简写,看看下面的代码

var a=2;
var o1={
    a:1,
    class(){
        console.log(`o1:${this.a}`);
    }
}
var o2={
    a:1,
    'class':function(){
        console.log(`o2:${this.a}`);
    }
}
var o3={
    a:1,
    'class':()=>{
        console.log(`o3:${this.a}`);
    }
}
o1.class();
o2.class();
o3.class();

// o1:1
// o2:1
// o3:2

上面的代码看到,简写的方法和箭头函数是不一样的,其只代表了function(){}的写法。

实际上,方法的简写的名字是对象调用方法时的属性名,而没法通过简写写出内部方法引用的函数名,看下面的代码

var o1={    
    'f':function fn(val){
        if(val>1){
            console.log(val);
            fn(val-1);
        }
    }
}
o1.f(3);
// 3
// 2


var o2={
    'f':function fn(val){
        if(val>1){
            console.log(val);
            this.f(val-1);
        }
    }
}
o2.f(3);
// 3
// 2


//简便写法
var obj={
    fn(val){
        if(val>1){
            console.log(val);
            this.fn(val-1);
        }
    }
}
obj.fn(3);
// 3
// 2

var obj={
    fn(val){
        if(val>1){
            console.log(val);
            fn(val-1);
        }
    }
}
obj.fn(3);
// 3
// Uncaught ReferenceError: fn is not defined at Object.fn

可以看到,当要在内部调用方法时,提示没有该方法,而其“父类”Object也没有该方法,所以最后报错了。所以简便写法中的“名字”,即这里的'fn',实际上是对象调用该方法时需要的属性名。

属性名表达式

在ES5中,使用字面量定义对象不能使用表达式来定义,只能使用下面的方法

var obj = {
  foo: true,
  abc: 123
};

但是在ES6中,也可以通过将表达式放在方括号里面作为属性名,方法也同样可以使用该属性名表达式的方法来定义。

let propKey = 'foo';
let obj = {
  [propKey]: true,
  ['a' + 'bc']: 123,
  [1+2]: 3,
  ['h' + 'ello']() {
    return 'hi';
  }
};



obj[3]//3

obj.foo//true

obj.abc//123

obj.hello() // hi

要注意的是,属性名表达式不能和简洁写法同时使用,否则会报错。

// 报错

const foo = 'bar';

const bar = 'abc';

const baz = { [foo] };

// 正确

const foo = 'bar';

const baz = { [foo]: 'abc'};

若属性名表达式为一个对象,默认情况下会讲该对象转为字符串[object Object]

const keyA = {a: 1};

const keyB = {b: 2};



const myObject = {

  [keyA]: 'valueA',

  [keyB]: 'valueB'

};



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

在上面代码中keyA和keyB都是一个对象,所以最后的属性名都为[object Object],因为属性名相同,所以后面的keyB的值覆盖了keyA的值。

属性的遍历


可枚举性

enumberable属性称为可枚举性,若该属性值为false,某些遍历操作无法遍历到该属性。

目前,有四个操作会忽略enumerable为false的属性。

for...in循环:只遍历对象自身的和继承的可枚举的属性的键名。

Object.keys():返回对象自身的所有可枚举的属性的键名。

JSON.stringify():只串行化对象自身的可枚举的属性。

Object.assign(): 忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。

这四个遍历操作中,只有for...in循环会遍历继承的属性,在阮一峰的ES6入门中提到之所以引入可枚举这个概念就是为了防止一些内部属性方法被遍历到,就如toString和length,其实也很好理解,有时我们只想遍历自己定义的属性,但是在使用for...in时却将继承的所有内部方法全都返回出来了,很难找到自己定义的属性方法。

通过使用Object.getOwnPropertyDescriptor方法,我们可以获取属性的描述对象。

let obj = { foo: 123 };

Object.getOwnPropertyDescriptor(obj, 'foo')

//  {

//    value: 123,

//    writable: true

//    enumerable: true,

//    configurable: true

//  }

Object.getOwnPropertyDescriptor(Object.prototype, 'toString')

//{

//     value: ƒ,

//     writable: true,

//     enumerable: false,

//     configurable: true

}

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

Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable

// false

遍历对象属性的方法

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 键,按照加入时间升序排列。
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })

// ['2', '10', 'b', 'a', Symbol()]

上面代码中,Reflect.ownKeys方法返回一个数组,包含了参数对象的所有属性。这个数组的属性次序是这样的,首先是数值属性2和10,其次是字符串属性b和a,最后是 Symbol 属性。

Super关键字


super关键字用于指向当前对象的原型对象

const proto = {
  foo: 'hello'
};

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

Object.setPrototypeOf(obj, proto);

obj.find() // "hello"

目前只有对象方法的简写才能让JavaScript引擎确认,下面三种写法都会报错

const obj = {
  foo: super.foo
}//Uncaught SyntaxError: 'super' keyword unexpected here

const obj = {
  foo: () => super.foo
}//Uncaught SyntaxError: 'super' keyword unexpected here

const obj = {
  foo: function () {
    return super.foo
  }
}//Uncaught SyntaxError: 'super' keyword unexpected here

在JavaScript引擎内部,super.foo等同于Object.getPrototypeOf(this).foo(属性)或Object.getPrototypeOf(this).foo.call(this)(方法)。即在使用super调用方法时,被调用的方法中若有this,则this绑定的还是当前对象,而非原型对象。

const proto = {

  x: 'hello',

  foo() {

    console.log(this.x);

  },

};



const obj = {

  x: 'world',

  foo() {

    super.foo();

  }

}



Object.setPrototypeOf(obj, proto);



obj.foo() // "world"

对象的扩展运算符


在ES2018中,引入了对象的扩展运算符

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

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

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

{...{}, a: 1}

// { a: 1 }

{...{}}

//{}

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

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

{...true} // {}

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

{...undefined} // {}

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

{...null} // {}

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

{...'hello'}

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

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

let ab = { ...a, ...b };

// 等同于

let ab = Object.assign({}, a, b);

若用户自定义的属性在扩展运算符之后,那扩展运算符中同名的属性会被覆盖

a={x:5,y:6,z:7}

//{x: 5, y: 6, z: 7}

let aWithOverrides = { ...a, x: 1, y: 2 };

let aWithOverrides = { ...a, ...{ x: 1, y: 2 } };

//等同于let x = 1, y = 2, aWithOverrides = { ...a, x, y };

//等同于let aWithOverrides = Object.assign({}, a, { x: 1, y: 2 });

aWithOverrides

//{x: 1, y: 2, z: 7}  

上面代码中,对象a中有属性x,y,但是因为a较早赋值给新对象aWithOverrides,由于在对象中,如果要给一个对象赋以相同属性名的键值对,那后赋值的后覆盖前赋值的,所以后面的x,y覆盖了对象a中的x,y。

相反,如果我们把扩展运算符放在自定义的属性后面,那么这些自定义的属性就可以当成是默认值了,在扩展运算符内没有相同属性名时使用这些自定义的属性,如果有同名的属性的话,就覆盖自定义的属性。

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

const obj = {

  ...(x > 1 ? {a: 1} : {}),

  b: 2,};

扩展运算符的参数对象之中,如果有取值函数get,这个函数是会执行的。

// 并不会抛出错误,因为 x 属性只是被定义,但没执行let aWithXGetter = {

  ...a,

  get x() {

    throw new Error('not throw yet');

  }

};

// 会抛出错误,因为 x 属性被执行了let runtimeError = {

  ...a,

  ...{

    get x() {

      throw new Error('throw now');

    }

  }

};

对象的新增方法


Object.is

该方法用于比较两个值是否严格相等,该方法传入两个参数,即用来比较的两个值。

在===严格相等中,存在不符合我们正常逻辑的两种情况,即NaN不等于自身,+0等于-0,而Object.is解决了这种不合正常逻辑的情况。

Object.is(1,1)  // true

Object.is(1,'1')  // false

Object.is(NaN,NaN)  // true

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

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

Object.defineProperty(Object, 'is', {

  value: function(x, y) {

    if (x === y) {

      // 针对+0 不等于 -0的情况   利用1/0等于Infinity而1/-0等于-Infinity的情况
      return x !== 0 || 1 / x === 1 / y;

    }

    // 针对NaN的情况   利用NaN!== NaN为true的情况  
    return x !== x && y !== y;

  },

  configurable: true,

  enumerable: false,

  writable: true

});

如果在进行+0和-0,NaN自身的特殊比较之外的比较时,最好不要使用Object.is,而是使用==或===,因为后者效率更高。

Object.assign

该方法的第一个参数为目标对象,后面的参数为源对象,该方法将源对象的可遍历属性复制到目标对象上,返回操作完成后的目标对象。如果没有源对象,则直接返回目标对象。

var t={};

Object.assign(t);

//{}

var a={a:1};

var b={b:2};

Object.assign(t,a,b);

t;

//{a: 1, b: 2}

若目标对象参数位置不是对象,方法会先将该参数转为对象后才进行操作

Object.assign(1)

//Number {1}

Object.assign('abc')

//String {"abc"}

Object.assign(true)

//Boolean {true}

但是如果传入的第一个参数是undefined或null的话,由于undefined和null没法转为对象,所以浏览器会报错,同样的,没有传入参数也会报错。

Object.assign(null)

//Uncaught TypeError: Cannot convert undefined or null to objec tat Function.assign

Object.assign(undefined)

//Uncaught TypeError: Cannot convert undefined or null to objec tat Function.assign

Object.assign()

//Uncaught TypeError: Cannot convert undefined or null to objec tat Function.assign

非第一位参数传入非对象数据结构会先转化为对象,对应不同数据结构有不同的处理方式

  1. 无法转为对象的数据结构,即null和undefined,但不同于第一个参数的是,在源对象位置传入这两个值,不会报错,会被跳过,对最终的目标对象没有影响;
  2. 能转为对象的非字符串结构,这类结构虽然能转为对象,但同样不会对目标对象产生影响
  3. 字符串,字符串是在该方法中传入的非对象数据结构中唯一能改变目标对象的数据结构,字符串会转化为数组对象的结构将可枚举的属性复制到目标对象中。
var tar={1:1}

Object.assign(tar,null)

//{1: 1}

Object.assign(tar,undefined)

//{1: 1}

Object.assign(tar,1)

//{1: 1}

Object.assign(tar,true)

//{1: 1}

Object.assign(tar,'abc')

//{0: "a", 1: "b", 2: "c"}

在上面的代码中能看到,原对象中的属性键名为1的属性值被传入的字符串对象中的属性覆盖了,这是因为在Object.assign方法中,后传入的属性会覆盖之前的同名属性。

之所以只有字符串能影响到目标对象,是因为数值,布尔值,字符串在转为对象时,会将原始值包装在对象的内部属性[[PrimitiveValue]]上面,而这个属性是不会被Object.assign拷贝的,即没法枚举,所以无法影响目标对象,而因为字符串会产生可枚举的属性,所以能影响到目标属性。

属性名为 Symbol 值的属性,也会被Object.assign拷贝。

Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' })

// { a: 'b', Symbol(c): 'd' }

使用该方法有几个注意点

1.该方法为浅拷贝,若复制的属性中有值为对象的,那么源对象的属性值发生改变,目标对象也会发生改变

var tar={};

var sou={obj:{a:1}};

Object.assign(tar,sou);

sou.obj.a=3;

tar.obj.a;

//3

2.同名属性的替换

这个在上面的代码已经提过了,后面的同名属性会覆盖之前的属性

3.处理数组的方式

Object.assing方法在处理数组时,会将数组对应的值的属性名变为其索引转字符串。

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

// [4, 5, 3]

4.取值函数的处理

Object.assign只能对值进行复制,如果要处理的是一个取值函数,那么会将取得的值赋值给该属性名

const source = {

  get foo() { return 1 }

};

const target = {};



Object.assign(target, source)

// { foo: 1 }

Object.getOwnPropertyDescriptors()

该方法用于返回对象的所有非继承属性的描述对象

var obj={

    a:1

}

Object.getOwnPropertyDescriptors(obj)

//a:{value: 1, writable: true, enumerable: true, configurable: true}

该方法可以由下面的代码来实现

function getOwnPropertyDescriptors(obj) {

  const result = {};

  for (let key of Reflect.ownKeys(obj)) {

    result[key] = Object.getOwnPropertyDescriptor(obj, key);

  }

  return result;

}

该方法通过使用for...of来遍历Reflect.ownKeys得到的keys,将key作为属性名赋给result,对应的属性值就使用Object.getOwnPropertyDescriptor获取的对应属性的描述对象,这样最后的result就是obj的所有属性的描述对象。

Object.getOwnPropertyDescriptors()方法的引入,解决了Object.assign方法只能拷贝属性的值,没法正确拷贝set和get方法的问题。通过结合Object.defineProperties方法就能实现正确的拷贝

const shallowMerge = (target, source) => Object.defineProperties(

  target,

  Object.getOwnPropertyDescriptors(source));

Object.defineProperties方法是用于定义对象的属性和方法的,这里将使用Object.getOwnPropertyDescriptors的对象的所有属性的描述对象作为Object.defineProperties方法的参数,就等于将所有的方法拷贝到新的对象target上。

类似于结合Object.defineProperties拷贝对象,也可以通过配合Object.create方法来克隆一个对象。这种克隆属于浅拷贝。

const clone = Object.create(Object.getPrototypeOf(obj),

  Object.getOwnPropertyDescriptors(obj));

// 或者

const shallowClone = (obj) => Object.create(

  Object.getPrototypeOf(obj),

  Object.getOwnPropertyDescriptors(obj));

先通过Object.getPrototypeOf方法获取对象的原型,再将非继承的属性方法用getOwnPropertyDescriptors获得,实现克隆。同样的方法也可以用于继承,如下面的代码实现继承

const obj = Object.create(

  prot,

  Object.getOwnPropertyDescriptors({

    foo: 123,

  }));

将要继承的对象放在Object.create的第一个参数的位置,然后将要创建的对象的描述对象写在后面的参数,这里就使用Object.getPrototypeOf来获取

原型对象的操作方法

__proto__属性

该属性用来读取或设置prototype对象,但只有浏览器必须部署这个属性,其他运行环境不一定要部署。尽可能不要使用该属性来读写prototype对象,而是使用Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。

实现上,__proto__调用的是Object.prototype.__proto__,具体实现如下。

Object.defineProperty(Object.prototype, '__proto__', {

  get() {

    let _thisObj = Object(this);

    return Object.getPrototypeOf(_thisObj);//返回该对象

  },

  set(proto) {

    if (this === undefined || this === null) {

      throw new TypeError();//对undefined或null设置其_proto_报错

    }

    if (!isObject(this)) {

      return undefined;//操作的数据结构为非对象返回undefined

    }

    if (!isObject(proto)) {

      return undefined;//将_proto_设置为非对象返回undefined

    }

    let status = Reflect.setPrototypeOf(this, proto);

    if (!status) {

      throw new TypeError();

    }

  },});

function isObject(value) {

  return Object(value) === value;//判断一个数据结构转为对象后是否严格等于自己,即判断是否为对象

}

Object.setPrototypeOf

该方法用于设置对象的prototype对象,返回设置的对象本身。

该方法传入两个参数,第一个参数为要设置的对象,第二个参数为要修改成的prototype对象。

var obj={};

var pro=null;

Object.setPrototypeOf(obj,pro);

// {}

如果第一个参数不是对象,那该方法会先将其转为对象,但是最后返回的仍是第一个参数

typeof Object.setPrototypeOf(true,pro)

//"boolean"

typeof Object.setPrototypeOf(1,pro)

//"number"

typeof Object.setPrototypeOf('123',pro)

//"string"

上面代码可以看到,虽然在方法的处理中会将其转为对象,但最后返回的值却仍是其原来的数据结构。

同样的,undefined和null因为无法转为对象,所以将其传入第一个参数会报错

Object.setPrototypeOf(undefined,pro)

//Uncaught TypeError: Object.setPrototypeOf called on null or undefined at Function.setPrototypeOf

Object.setPrototypeOf(null,pro)

//Uncaught TypeError: Object.setPrototypeOf called on null or undefined at Function.setPrototypeOf

Object.getPrototypeOf

该方法用于获取对象的prototype对象,返回获取的prototype对象,该方法只传入一个参数,即要获取的对象。

pro={}

Object.setPrototypeOf(obj,pro)

Object.getPrototypeOf(obj)

//{}

其对非对象的参数的处理与setPrototypeOf一样。

获取可遍历属性的方法

下列三个方法都返回一个数组,但数组成员有所不同

Object.keys():传入对象自身的非继承的可遍历的属性的属性名

Object.values():传入对象自身的非继承的可遍历的属性的属性值

Object.entries():传入对象自身的非继承的可遍历的属性的值对

var obj={

    a:1,

    b:2

}

Object.keys(obj)

// ["a", "b"]

Object.values(obj)

// [1, 2]

Object.entries(obj)

// [["a", 1], ["b", 2]]

Object.fromEntries

该方法与Object.entries正好相反,用于将键值对数组转为对象

(在写这篇博客时该方法还不能广泛使用,至少在chrome浏览器控制台尝试时用不了)

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 }

参考自阮一峰的《ECMAScript6入门》

           Kyle Simpson的《你不知道的JavaScript 下卷》


ES6学习笔记目录(持续更新中)

 

ES6学习笔记(一)let和const

ES6学习笔记(二)参数的默认值和rest参数

ES6学习笔记(三)箭头函数简化数组函数的使用

ES6学习笔记(四)解构赋值

ES6学习笔记(五)Set结构和Map结构

ES6学习笔记(六)Iterator接口

ES6学习笔记(七)数组的扩展

ES6学习笔记(八)字符串的扩展

ES6学习笔记(九)数值的扩展

ES6学习笔记(十)对象的扩展

ES6学习笔记(十一)Symbol

ES6学习笔记(十二)Proxy代理

ES6学习笔记(十三)Reflect对象

ES6学习笔记(十四)Promise对象

ES6学习笔记(十五)Generator函数

ES6学习笔记(十六)asnyc函数

ES6学习笔记(十七)类class

ES6学习笔记(十八)尾调用优化

ES6学习笔记(十九)正则的扩展

ES6学习笔记(二十)ES Module的语法

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值