文章将会对对象属性特性,原型链,对象拷贝进行说明。
一、JavaScript属性的可迭代、可修改和可配置特性
作者: Javier Márquez
原题:JavaScript properties are enumerable, writable and configurable
摘要:对属性的属性做了定义并通过示例说明了者几个属性的属性的应用场景, 比如不可枚举用于序列化、不可修改用于常量、不可配置用于完全不可变对象.
对象是JavaScript的重要部分. JavaScript 的对象语法十分精确并容易使用,所以我们经常将对象作为 HashMap 使用。
你知道上面代码中的对象的所有属性都是可迭代、可修改和可配置的吗?
可迭代,意味着我能够通过 for..in 循环来访问该对象的所有属性. 还能通过 Object.keys() 方法获取该对象的所有属性名.
可修改,意味着我能修改该对象的所有属性的值,通过为这些属性赋予一个新值就能修改: ob.a = 1000;.
可配置,意味着我能修改属性的行为,让该对象的属性都是不可迭代的、不可修改的和不可配置的. 只有可配置的属性才能通过 delete 被删除.
我感打赌你知道Object属性的前两个特性(译者注:这里用特性描述属性的属性,而属性用来描述对象的属性.), 但是只有少数人知道通过调用 Object 的 defineProperty 方法能够创建对象的属性、修改对象的属性为不可迭代的和不可变的.
我估计这种语法并不像平常的语法那么常见, 但是使用这种属性在解决某些问题时确实很方便. defineProperty 方法中定义对象属性的对象称为描述符(descriptor),可以通过 getOwnPropertyDescriptor 方法来查看任何属性的描述符.
有趣的是Object.defineProperty中属性描述符的默认选项值与上面的例子中的值正好相反:描述符默认是不可迭代、不可修改和不可配置的.
此外,还可以在通过Object.create(prototype, properties)方法创建对象时指定对象属性的描述符.
前面说过, 可枚举属性可以通过for..in循环来访问, 而不可枚举属性不能. 基本上来说, 非枚举属性对于大多数将对象作为 HashMap 来使用的对象来说都是不能使用的.
不可枚举属性不能通过for..in迭代
Object.keys 函数不能返回
不能通过JSON.stringify序列化为JSON字符串
所以这类属性就像是某种秘密属性, 但是仍然能够访问.
由于这类属性不能序列化, 我发现在处理数据模型对象时特别有用. 能够通过使用不可枚举属性来方便的添加信息到数据模型对象中.
考虑一下,如果要创建一个ORM库的话,如果有该特性,那会有多方便.
如果要获取对象的所有属性, 包括可枚举的和不可枚举的, 可以通过Object.getOwnPropertyNames获取对象的所有属性名.
虽然到ES6的时候我们就有一直以来都期望的const语句, 但现在我们就能通过可修改属性模拟常量. 不可修改的属性的属性值是不可修改的.
正如输出结果所示, 对 ob.B 的赋值并没有影响它的值. 这里需要需要,赋值语句总会返回被赋予的值, 无论赋值是否成功. 比如, 这里的不可修改属性就是赋值失败的. 而在严格模式(strict mode)下,尝试对不可修改属性赋值会引发TypeError异常.
此外, 如果不可修改属性的值是一个对象的话, 那么对该对象的引用是不可修改的, 但是引用的对象的属性是可以修改的.
如果想要让属性完全的不可修改, 可以用Object.freeze函数. 它让对对象的属性的添加、删除、修改都无效. 而且在严格模式下还会引发 TypeError 异常.
如果上面的对象是定义为可配置的,那么你还对其进行修改. 还可以通过defineProperty来该变属性为可修改的或不可迭代的. 但是如果一旦定义属性为不可配置的,那么能做的事就只有一件了: 如果该属性是可修改的, 那么可以将其改为不可修改的. 其他任何类型的更新都将引发 TypeError 异常.
特别重要的一点, 可配置属性能能够通过 delete 操作符从对象中移除. 所以, 如果你创建了一个不可配置和不可修改的属性, 那该属性就是一个被冻结的属性.
Object.defineProperty是从ES5引入的, 现在就可以开始使用了, 其被所有现代浏览器支持, 包括 IE9(甚至是 IE8, 但其仅支持 DOM 对象). 通过不同的方式来实现我们之前常常做的任务总是很有趣, 并且通过观察 JavaScript 的核心(即对象)是如何工作的, 我们能够轻松的学习新知识.
Object.defineProperty还给了我们能够自定义getter和setter的自由, 不过今天的文章没有涉及到. 如果想要了解更多, 那就去看看一定会令人有意外收获的Mozilla’ Documentation吧.
二、JS原型
JS原型可以说是JS对于OOP的一种转变和兼容,其可以理解为JS对于继承的一种实现。关键字是prototype。这一关键字是我们JS的函数对象之中所特有的。而有些浏览器在控制台打印出对象的时候会有_proto_属性,实际上这一属性不是标准属性,只是部分浏览器实现了而已。
紧接着直接上第二张图说明
由此可见其实prototype和_proto_是不太相同的两个属性。最后来看一下什么是原型链
上一张国外大神给的图片:
弄清楚了这张图实际上原型差不多就基本弄通了,但是有几个特别的方面我们是需要注意一下的。
1.当我们在设置了原型的时候生成了对象,但是这是如果原型修改过了,之前生成的对象的原型是无法改变的。这点要特别注意。
2.原型对象之中会有一个constructor属性,这一属性在我们定义原型的时候会自动的指向我们的构造函数,但是某些情况下我们修改原型之后这一属性并不会自动指向,这是我们就需要进行修改,以确保原型链的正确性。
三:对象拷贝
对象拷贝分为深拷贝和浅拷贝。
1.浅拷贝:我们使用的对象的赋值运算实际上就算是浅拷贝的一种了,其拷贝的实际上是对象的引用。总结可以为:浅拷贝指两个不同的变量存的是同一个对象的地址,即两个变量指向同一块内存区域.
2.深拷贝:深拷贝则不是引用的拷贝了,而是开阔一块新的区域对对象之中的可枚举属性进行全面复制,这是候两个变量指向的区域是完全不相同的。
JS之中提供了一个方法为Object.assign(target, obj1, obj2),在MDN上面描述为将所有可枚举属性的值从一个或多个源对象复制到目标对象。String类型和 Symbol 类型的属性都会被拷贝, 并且该方法会忽略值为 或者 undefined 的源对象。所以可以说这个方法是一个介于浅拷贝和深拷贝之间的内容。
-- 当我们将target值设置成为{}的时候。并且其被拷贝对象之中没有引用类型的时候,则其实际上就是一次深拷贝。
-- 当target为{}的时候,且被拷贝对象之中有引用类型的时候,则新的对象指向的同一块内存区域,即对引用类型进行了浅拷贝。
有上面两点我们可以看出assign方法实际上是对属性进行了拷贝。而且,如果背靠背对象之中有属性相同的话,则之后的参数会将前面参数的拷贝获取的值给覆盖。
Object.assign 拷贝的是对象的可枚举属性,该方法使用源对象的 [[Get]] 和目标对象的 [[Set]],所以它会调用相关 getter 和 setter。因此,不如说它是分配属性,而不仅仅是复制或定义新的属性。如果合并源包含 getter,这可能使其不适合将新属性合并到原型中,将属性定义(包括其可枚举性)复制到原型应使用Object.getOwnPropertyDescriptor() 和 Object.defineProperty() ,因此 Object.assign 不能拷贝对象的继承属性
ECMAScript 有 5 种原始类型(primitive type): Undefined、Null、Boolean、Number 和 String。当Object.assign的源对象是原始类型时,源对象会被包装成“对象”,对应的键是它在源对象中的索引值:
在出现错误的情况下,例如,如果属性不可写,会引发TypeError,异常会打断后续的拷贝任务。如果在引发错误之前添加了任何属性,则可以更改target对象。