ES6——对象的扩展详解

对象的扩展详解

1、属性的简洁表示法

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

js 复制代码
let foo = 'bar';
let baz = {foo};
console.log(baz);//{ foo: 'bar' }
//等同于
// let baz = {foo: foo};

上面的代码表明,ES6允许在对象中只写属性名,不写属性值。这时,属性值等于属性名所代表的变量。下面是另一个例子。

js 复制代码
function f(x, y) {
    return {x, y};
}
//等价于
function f(x, y) {
    return {x: x, y: y};
}
console.log(f(1, 2));//{ x: 1, y: 2 }

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

js 复制代码
let o = {
    method() {
        return "Hello";
    }
};
//等价于
let o = {
    method: function() {
        return "Hello";
    }
};

下面是一个实际的例子。

js 复制代码
let Person = {
    name: '张三',
    //等同于birth: birth
    birth,
    //等同于hello: function(){}
    hello() {
        console.log(this.name);
    }
}

这种写法用于函数的返回值会非常方便。

js 复制代码
function getPoint() {
    let x = 1;
    let y = 10;
    return {x, y};
}

console.log(getPoint());//{ x: 1, y: 10 }

CommonJS模块输出变量就非常适合使用简洁写法。

js 复制代码
let ms = {};

function getItem(key) {
    return key in ms ? ms[key] : null;
}

function setItem(key, value) {
    ms[key] = value;
}

function clear() {
    ms = {};
}

module.exports = {getItem, setItem, clear};
//等价于
module.exports = {
    getItem: getItem,
    setItem: setItem,
    clear: clear
};

属性的赋值器(setter)和取值器(getter)事实上也采用了这种写法。

js 复制代码
let cart = {
    _wheels: 4,
    get whiles () {
       return this._wheels; 
    },
    set whiles (value) {
        if(value < this._wheels) {
            throw new Error("too small!");
        }
        this._wheels = value;
    }
}

注意,简洁写法中属性名总是字符串,这会导致一些看上去比较奇怪的结果。

js 复制代码
let obj = {
	class () {}
};
//等同于
let obj = {
	'class' : function(){}
};

上面的代码中,class是字符串,所以不会因为它属于关键字而导致语法解析报错。

如果某个方法的值是一个Generator函数,则其前面需要加上星号。

js 复制代码
let obj = {
    * m(){
        yield 'hello world'
    }
}

2、属性名表达式

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

js 复制代码
//方法一
obj.foo = true

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

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

但是,如果使用字面量方式定义对象(使用大括号)​,在ES5中只能使用方法一(标识符)定义属性。

js 复制代码
let obj = {
	foo: true,
	abc: 123
}

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

js 复制代码
let propKey = 'foo';
let obj = {
    [propKey] : true,
    ['a' + 'bc'] : 123
};

下面是另一个例子。

js 复制代码
let lastWord = 'last word';
let a = {
    'first word': 'hello',
    [lastWord]: 'world'
};
console.log(a['first word']);//hello
console.log(a[lastWord]);//world
console.log(a['last word']);//world

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

js 复制代码
let obj = {
    ['h' + 'ello']() {
        return 'hi';
    }
}

obj.hello();//hi

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

3、方法的name属性

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

js 复制代码
let person = {
    sayName: function() {
        console.log(this.name);
    }
}

console.log(person.sayName.name);//sayName

有两种特殊情况:

  • bind方法创造的函数,name属性返回"bound"加上原函数的名字;Function构造函数创造的函数,name属性返回"anonymous"。
  • 如果对象的方法是一个Symbol值,那么name属性返回的是这个Symbol值的描述。

4、Object.is()

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

js 复制代码
console.log(Object.is('foo', 'foo'));//true
console.log(Object.is({}, {}));//false

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

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

js 复制代码
Object.defineProperty(Object, 'is', {
    value: function(x, y) {
        if (x === y) {
            return x !== 0 || 1 / x === 1 / y;
        }
        return x !== x && y !== y;
    },
    configurable: true,
    enumerable: false,
    writable: true
});

5、Object.assign()

5.1、基本用法

Object.assign方法用来将源对象(source)的所有可枚举属性复制到目标对象(target)。它至少需要两个对象作为参数,第一个参数是目标对象,后面的参数都是源对象。只要有一个参数不是对象,就会抛出TypeError错误。

js 复制代码
let target = {a: 1};
let source1 = {b: 2};
let source2 = {c: 3};
console.log(Object.assign(target, source1, source2));//{ a: 1, b: 2, c: 3 }

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

js 复制代码
let target = {a: 1, b: 1};
let source1 = {b: 2, c: 2};
let source2 = {c: 3};
console.log(Object.assign(target, source1, source2));//{ a: 1, b: 2, c: 3 }

Object.assign只复制自身属性,不可枚举的属性(enumerable为false)和继承的属性不会被复制。

js 复制代码
Object.assign({b: 'c'}, 
    Object.defineProperty({}, 'invisible', {
        enumerable: false,
        value: 'hello'
    }));
//{b: 'c'}

上面的代码中,Object.assign要复制的对象只有一个不可枚举属性invisible,这个属性并没有被复制进去。

属性名为Symbol值的属性,也会被Object.assign复制。

js 复制代码
Object.assign({a: 'b'}, {[Symbol('c')]: 'd'});
//{a: 'b', Symbol(c): 'd'}

对于嵌套的对象,Object.assign的处理方法是替换,而不是添加。

js 复制代码
let target = {a: {b: 'c', d: 'e'}};
let source = {a: {b: 'helo'}};
Object.assign(target, source)
//{a: {b: 'hello'}}

上面的代码中,target对象的a属性被source对象的a属性整个替换掉了,不会得到{a:{b:ˈhelloˈ,d:ˈeˈ}}的结果。这通常不是开发者想要的,需要特别小心。有一些函数库提供Object.assign的定制版本(比如Lodash的_.defaultsDeep方法)​,可以解决深复制的问题。注意,Object.assign可用于处理数组,但是会将其视为对象。

js 复制代码
Object.assign([1, 2, 3], [4,5])
//[4, 5, 3]

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

5.2、应用

为对象添加属性

js 复制代码
class Point {
    constructor(x, y) {
        Object.assign(this, {x, y});
    }
}

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

为对象添加方法

js 复制代码
Object.assign(SomeClass.prototype, {
    someMethod(arg1, arg2){
        ...
    },
    anotherMethod() {
        ...
    }
});

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

克隆对象

js 复制代码
function clone(origin) {
    return Object.assign({}, origin);
}

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

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

js 复制代码
function clone(origin) {
    let originProto = Object.getPrototypeOf(origin);
    return Object.assign(Object.create(originProto), origin);
}

合并多个对象:

将多个对象合并到某个对象。

js 复制代码
const merge = (target, ...source) => Object.assign(target, ...source);

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

js 复制代码
const merge = (...source) => Object.assign({}, ...source);

为属性指定默认值

js 复制代码
const DEFAULTS = {
    logLevel: 0,
    outputFormat: 'html'
};
function processContent(options) {
    let options = Object.assign({}, DEFAULTS, options);
}

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

注意,由于存在深复制的问题,DEFAULTS对象和options对象的所有属性的值都只能是简单类型,而不能指向另一个对象。否则,将导致DEFAULTS对象的该属性不起作用。

6、属性的可枚举性

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

js 复制代码
let obj = {foo: 123};
let x = Object.getOwnPropertyDescriptor(obj, 'foo');
console.log(x);//{ value: 123, writable: true, enumerable: true, configurable: true }

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

ES5有3个操作会忽略enumerable为false的属性。

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

ES6新增了2个操作,会忽略enumerable为false的属性。

  • Object.assign():只复制对象自身的可枚举属性。
  • Reflect.enumerate():返回所有for...in循环会遍历的属性。

这5个操作中,只有for...in和Reflect.enumerate()会返回继承的属性。实际上,引入enumerable的最初目的,就是让某些属性可以规避掉for...in操作。比如,对象原型的toString方法,以及数组的length属性,就通过这种手段而不会被for...in遍历到。

js 复制代码
Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable;
//false

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

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

总的来说,操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用for...in循环,而用Object.keys()代替。

7、属性的遍历

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

  • for...in:for...in循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)。
  • Object.keys(obj):Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)。
  • Object.getOwnPropertyNames(obj):Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性)。
  • Object.getOwnPropertySymbols(obj):Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有Symbol属性。
  • Reflect.ownKeys(obj):Reflect.ownKeys返回一个数组,包含对象自身的所有属性,不管属性名是Symbol或字符串,也不管是否可枚举。
  • Reflect.enumerate(obj):Reflect.enumerate返回一个Iterator对象,遍历对象自身的和继承的所有可枚举属性(不含Symbol属性),与for...in循环相同。

以上6种方法遍历对象的属性遵守同样的属性遍历次序规则。

  • 首先遍历所有属性名为数值的属性,按照数字排序。
  • 其次遍历所有属性名为字符串的属性,按照生成时间排序。
  • 最后遍历所有属性名为Symbol值的属性,按照生成时间排序。
js 复制代码
let x = Reflect.ownKeys({[Symbol()]:0, b:0, 10:0, 2:0, a:0});
console.log(x);
//[ '2', '10', 'b', 'a', Symbol() ]

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

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

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

js 复制代码
//ES6的写法
let obj = {
    method: function() {...}
}
obj.__proto__ = someOtherObj;

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

该属性没有写入ES6的正文,而是写入了附录,原因是__proto__前后的双下画线说明它本质上是一个内部属性,而不是一个正式的对外的API,只是由于浏览器广泛支持,才被加入了ES6。标准明确规定,只有浏览器必须部署这个属性,其他运行环境不一定要部署,而且新的代码最好认为这个属性是不存在的。因此,无论从语义的角度,还是从兼容性的角度,都不要使用这个属性,而是使用Object.setPrototypeOf()(写操作)​、Object.getPrototypeOf()(读操作)或Object.create()(生成操作)代替。

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

js 复制代码
Object.defineProperty(Object.prototype, '__proto__', {
    get() {
        let _thisObj = Object(this);
        return Object.getPrototypeOf(_thisObj);
    },
    set(proto) {
        if(this === undefined || this === null) {
            throw new TypeError();
        }
        if(!isObject(this)) {
            return undefined;
        }
        if(!isObject(proto)) {
            return undefined;
        }
        let status = Reflect.setPrototypeOf(this, proto);
        if(!status) {
            throw new TypeError();
        }
    }
});
function isObject(value) {
    return Object(value) === value;
}

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

js 复制代码
Object.getPrototypeOf({__proto__: null})
//null
Object.setPrototypeOf()

Object.setPrototypeOf方法的作用与__proto__相同,用于设置一个对象的prototype对象。

它是ES6正式推荐的设置原型对象的方法。

js 复制代码
//格式
Object.setPrototypeOf(Object, prototype);
//用法
let o = Object.setPrototypeOf({}, null);

该方法等同于下面的函数。

js 复制代码
function (obj, proto) {
    obj.__proto__ = proto;
    return obj;
}

下面是一个例子。

js 复制代码
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对象的属性。

js 复制代码
Object.getPrototypeOf(obj)

该方法与setPrototypeOf方法配套,用于读取一个对象的prototype对象。

js 复制代码
function Rectangle() {
}
let rec = new Rectangle();
Object.getPrototypeOf(rec) === Rectangle.prototype;//true

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

9、对象的扩展运算符

9.1、Rest参数

Rest参数用于从一个对象取值,相当于将所有可遍历但尚未被读取的属性,分配到指定的对象上。所有的键及其值都会复制到新对象上。

js 复制代码
let {x, y, ...z} = {x: 1, y: 2, a: 3, b: 4};
//x=1
//y=2
//z={a:3, b:4}

上面的代码中,变量z是rest参数所在的对象。它获取等号右边的所有尚未读取的键(a和b)​,将它们及其值复制过来。

注意,rest参数的复制是浅复制,即如果一个键的值是复合类型的值(数组、对象、函数)​,那么rest参数复制的是这个值的引用,而不是这个值的副本。

js 复制代码
let obj = {a: {b: 1}};
let {...x} = obj;
obj.a.b = 2;
console.log(x.a.b);//2

上面的代码中,x是rest参数,复制了对象obj的a属性。a属性引用了一个对象,修改这个对象的值会影响到rest参数对它的引用。

另外,rest参数不会复制继承自原型对象的属性。

js 复制代码
let o1 = {a: 1};
let o2 = {b: 2};
o2.__proto__ = o1;
let o3 = {...o2};
console.log(o3);//{ b: 2 }

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

9.2、扩展运算符

扩展运算符用于取出参数对象的所有可遍历属性,复制到当前对象中。

js 复制代码
let z = {a: 3, b: 4};
let n = {...z};
console.log(n);//{ a: 3, b: 4 }

这等同于使用Object.assign方法。

js 复制代码
let aClone = {...a};
//等同于
let aClone = Object.assign({}, a);

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

js 复制代码
let ab = {...a, ...b}

扩展运算符还可以用于自定义属性,会在新对象中覆盖掉原有参数。

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

上面的代码中,a对象的x属性和y属性,复制到新对象后会被覆盖掉。

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

js 复制代码
let aWithDefaults = {x: 1, y: 2, ...a};
//等同于
let aWithDefaults = Object.assign({}, {x:1, y:2}, a);
//等同于
let aWithDefaults = Object.assign({x:1, y:2}, a);

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

js 复制代码
//并不会抛出错误,因为x属性只是被定义,但未执行
let aWithXGetter = {
    ...addEventListener,
    get x(){
        throw new Error('not throw yet');
    }
};
//会抛出错误,因为x属性被执行了
let funtimeError = {
    ...a,
    ...{
        get x() {
            throw new Error('throw now');
        }
    }
};

如果扩展运算符的参数是null或undefined,则会被忽略,不会报错。

js 复制代码
let emptyObject = {...null, ...undefined};//不报错
相关推荐
aq55356002 小时前
编程语言对比:从汇编到PHP的四大层级解析
开发语言·汇编·php
kyle~2 小时前
工程数学---Eigen库(C++唯一标配线性代数库)
开发语言·c++·线性代数
CoderCodingNo2 小时前
【GESP】C++五、六级练习题 luogu-P1886 【模板】单调队列 / 滑动窗口
开发语言·c++·算法
好家伙VCC2 小时前
**发散创新:基于Rust的轻量级权限管理库设计与开源许可证实践**在现代分布式系统中,**权限控制(RBAC
java·开发语言·python·rust·开源
xiaoshuaishuai82 小时前
C# 方言识别
开发语言·windows·c#
John.Lewis2 小时前
C++进阶(6)C++11(2)
开发语言·c++·笔记
@atweiwei2 小时前
用 Rust 构建agent的 LLM 应用的高性能框架
开发语言·后端·rust·langchain·eclipse·llm·agent
skilllite作者2 小时前
Spec + Task 作为「开发协议层」:Rust 大模型辅助的标准化、harness 化与可回滚
开发语言·人工智能·后端·安全·架构·rust·rust沙箱
Dxy12393102162 小时前
Python序列标注模型上下文纠错详解
开发语言·python