最近在整理js基础方面相关的文档,整理到对象的时候发现这一块的内容看似简单但实际上却有不少容易忽视的点,本文针对于(《你不知道的Javascript》上卷的对象
)进行整理和总结,咱们先抛出几个问题:javascript
咱们先来聊下一些基本概念。java
咱们知道对象是建立无序键 / 值对数据结构(映射)的主要机制。对象的建立能够经过经常使用的两种形式:字面量和构造形式。数组
对象的字面量:数据结构
var obj = { key: 'value1', arr: [1, 2, 3], foo: function() { // todo } }
构造器形式:闭包
var obj = new Object(); obj.key = "value1" obj.arr = [1, 2, 3], obj.foo = function() { // todo }
其实 var obj = {}
是 new Object()
的语法糖,它们生成的对象是同样的。惟一的区别是便体如今了添加属性的时候,字面量能够添加多个键 / 值对,而构造形式必须逐个添加属性。函数
在 JavaScript 中最新的语言类型包括8种:this
简单基本类型(原始数据类型)除 object 之外,它们自己不是对象,但有些是存在它们的包装类型,如:number 的 Number(),string 的 String() 等。prototype
注意: 虽然typeof null === 'object'
为true,但这是js
自己的一个 bug ,修复的话能够会形成更多意料以外的 bug 。《你不知道的JavaScript》 上卷中对象一节说起到实际上不一样的对象在底层都表示为二进制,在
js
中二进制前三位都为 0 的话是被断定为object
类型的,而null
的二进制表示全是 0 ,因此执行typeof
的时候会返回object
。指针
与简单基本类型相对的是复杂类型,如 Function
、Array
,它们都属于对象的子类型。code
除了刚刚说起的function
、 array
,还有一类子类型对象即内置对象:
它们实际上都是内置函数,或者称它们为类,能够经过 new
运算符调用构造器产生。
咱们能够发现一些内置对象是跟基本类型相照应的,先看一下例子:
var str = "hello, 你好" str.indexOf(',') // 5 str.charAt(5) // , var number = 12.122; number.toFixed(2) // 12.12
这些列子说明了 js
引擎在编译运行的时候会先把基础类型的数据自动装箱,而后就能够调用其自身能够访问或者原型链上再或者顶级 Object
原型上定义的属性或方法了。
null
和 undefined
没有对应的包装类型,相反 Date
没有对应的字面量形式。
对象能够经过点(.
)语法访问属性值,也能够经过 []
相似数组访问值的形式访问属性值。ES6新增的可计算属性名即可以经过 []
这种语法来操做对象的属性:
var suffix = '.png' var imgConfig = { ['img1' + suffix]: 'http://abc.com', ['img2' + suffix]: 'http://abc.com', ['img3' + suffix]: 'http://abc.com' }
稍后咱们还会说起一个使用Symbol.iterator
做为计算属性名的自定义迭代器,能够配合for..of
使用。
针对于浅复制可使用 ES6 定义的 Object.assign
实现浅拷贝。可是针对于深拷贝的问题要复杂不少,必需要考虑不少状况,诸如各类类型的对象的拷贝,循环引用等问题,具体分析能够参见js之继承
在ES5以前,JavaScript 未提供能够表述对象属性及自身属性检查的方法。从ES5以后,咱们建立的对象都具有了属性描述符。
var obj = { name: "kkxiaoa" } Object.getOwnPropertyDescriptor(obj, 'name'); // { // configurable: true // enumerable: true // value: "kkxiaoa" // writable: true // }
在建立普通属性时属性描述符会使用默认值,能够Object.defineProperty
进行修改已有属性的描述,前提是它的 configurable
必须为 true
。
writable决定了属性是否能够被修改:
var obj = {}; Object.defineProperty(obj, 'id', { value: 1, writable: false, configurable: true, enumerable: true }) obj.id = 2; obj.id; // 1
在非严格模式下,修改一个只读属性的值默认会忽略。可是在严格模式下,则会抛出异常:
"use strict"; var obj = {}; Object.defineProperty(obj, 'id', { value: 1, writable: false, configurable: true, enumerable: true }) obj.id = 2; // TypeError
像这样便会报类型错误异常,表示没法修改只读属性的值。
默认建立对象的时候,属性 configurable
为 true
表示可使用 defineProperty
进行配置,可是当手动更改它的可配置属性为 false
的时候,再次使用 defineProperty
则会抛出异常:
var obj = {}; Object.defineProperty(obj, 'id', { value: 1, writable: true, configurable: false, enumerable: true }) obj.id; // 1 obj.id = 2; obj.id; // 2 Object.defineProperty(obj, 'id', { value: 1, writable: true, configurable: true, enumerable: true }) // TypeError Cannot redefine property: id
注意:
- 把 configurable 修改成 false 是单项操做,不能撤销
- 在
configurable
为false
的前提下仍然能够将writable
由true
置为false
可是没法由false
置为true
- 在
configurable
为false
的前提下该属性没法被删除
该属性表明对象的属性是否可被枚举,如 for..in
或 Object.keys
等遍历中就是经过该属性来遍历可枚举的属性。
注意: 这里的for..im
和in
操做符是两回事,in
操做符是检查该熟悉是否存在于制定对象中,不论它是否可枚举。
有时候咱们但愿定义一些常量,这时候可使用 const
关键字。可是若是咱们想定义一个常量类,这里面存放的是一些基础性的配置属性,咱们并不但愿它被扩展,属性被改写或删除,这个时候咱们可使用下列方法让这个对象密封或冻结。
咱们能够结合 writable: false
和 configurable: false 建立一个不可变的常量
,这样的话该属性不能被删除、重定义、修改:
var Constants = {}; Object.defineProperty(Constants, 'NUMBER_KEY', { value: 'AAFJJ1231', writable: false, configurable: false })
若是想要禁止一个对象添加新属性而且保留已有属性,可使用 Object.preventExtensions
:
var obj = { id: '111' } Object.preventExtensions(obj); obj.key = 'abc'; obj; // {id: '111'}
在严格模式下会报异常,在普通模式下会忽略扩展。
若是在禁止扩展
的前提下不想让属性进行配置删除操做(可修改)可使用 Object.seal()
,该方法会在现有的对象上使用Object.preventExtensions
,并把现有属性 configurable
更改成 false
。
若是想要在 密封
的前提下禁止修改对象属性,可使用 Object.freeze()
,该方法会在现有对象上调用Object.seal()
,把全部数据访问属性的 writable
置为 false
。
注意:使用freeze
方法只会冻结对象自己及任意直接属性,对于那些保存着对象引用的属性则不受该方法的影响。若是想要深度冻结,能够经过递归的方式遍历该对象,检测到引用对象的存在时使用freeze
方法,但这样可能会冻结掉全局
共享的对象,请当心使用。
属性的访问(不论是经过 .
或者 []
)访问属性的时候其实是实现了 [[Get]]
操做(相似于方法调用),当咱们访问某一属性的时候,如:obj.id
,语言内部首先在对象上查找是否具有相同名称的属性,存在则返回其值。不然会遍历该对象的原型链,存在的话返回其值。若是都不存在[[Get]]
操做会返回 undefined
:
var obj = { id: undefined }; obj.id; // undefined obj.key; // undefined
这两种都是返回 undefined
可是 obj.key
则会进行更复杂的处理,其不只仅是查找自身,还会遍历原型链。
咱们再看一个常见的例子:
function Foo(id) { this.id = id } Foo.prototype.getId = function() { return this.id } var a = new Foo(1); var b = new Foo(2); a.getId() // 1 b.getId() // 2
咱们都知道这样是能够访问的,可是仔细的品一下经过 Object.keys
(a) 它里面只有 id
一个属性,会什么能够访问到 getId()
方法呢? 答案就是经过 [[Get]]
的默认行为属性在自身不存在时检查原型链。
与 [[Get]]
操做相对应的即是 [[Put]]
操做,起初我认为给对象赋值便会触发[[Put]]
操做来实现编辑或者建立行为,可是真正当[[Put]]
被触发的时候,这里面有多种因素可能致使赋值不会使用默认行为,其中一个最终要的因素即是该属性是否存在于其自身:
若是该属性存在于其自身(非原型链)上,[[Put]]
操做将会进行以下检查:
Setter
则直接调用 Setter
。writable
是否为 false
?若是是,在普通模式下值会被忽略,而在严格模式下会抛 出 TypeError
异常。若是该属性存在于其原型链上,[[Put]]
操做会出现的三种状况:
writable
不为 false
,那么就会在该对象上添加一个同名的新属性并赋值。writable
为 false
,在严格模式下会抛出TypeError
异常,在普通模式下则会忽略本次赋值。Setter
那就会调用这个 Setter
,并不会添加新的属性到这个对象上。前面咱们讲到了数据描述符(用来描述数据的行为),与之相对应即是访问描述符(getter、setter
),若是属性定义了访问描述符(二者都存在的时候)JavaScript
会忽略它们的 value
和 writable
特性,取而代之的是get
、set
、configurable
、enumerable
的特性。
对象属性值的设置和获取默认使用 [[Set]]
和 [[Put]]
,ES5中可使用 Getter
和 Setter
改写对象的默认操做,它们都是隐藏函数。若是对某一属性设置了 getter
那么获取属性值的时候会被调用。同理,setter
则会在对属性设置值的时候被调用。
注意:getter
和setter
只能绑定到单个属性上面。若是想要为整个对象上的属性进行定义,则能够遍历对象使用defineProperty
对属性进行改造。如 Vue 中的变化侦测机制就是将整个对象使用getter
和setter
进行改造的。
咱们先来看下如何定义它们:
var person = { get name() { return 'kkxiaoa' } } Object.defineProperty(person, 'say', { get: function() { return 'hellow, my name is ' + this.name }, enumerable: true }) person; // {} 由于是隐藏函数,咱们并未定义属性 person.name; // 'kkxiaoa' person.say // 'hellow, my name is kkxiaoa' Object.keys(person) // ['name', 'say']
这是两种定义 getter
的方式,无论使用那一种,咱们看到都会建立一个不包含值的属性。咱们获取对应的属性的时候会自动调用 getter
隐藏函数。因为咱们只定义了 getter
,咱们设置属性值试试:
person.name = 'abc' person.name; // 'kkxiaoa'
能够看到对 name
赋值会被忽略,因为咱们并未定义 setter
结果是符合预期的。咱们接着定义一下 setter
,它会覆盖该属性的默认 [[Put]]
操做。
var person = { get name() { return 'hello, ' + this._name }, set name(name) { this._name = name // 定义一个新属性存储,不要使用name,不然会形成循环引用 } } person.name = 'world' person.name; // 'hello, world'
讲到遍历会想到刚刚咱们说起的属性描述符 enumerable
,它们是紧密关联的。对于对象的遍历咱们最多见的有for..in
、Object.keys
、for..of
,咱们先来谈谈关于存在性的问题。
咱们知道 in
和 hasOwnProperty
均可以判断属性是否存在于对象上,可是它俩的区别就是 in
除了自身外还会查找整个原型链是否存在该属性(不论该属性是否可枚举),hasOwnProperty
一样是检查的属性不管是否可被枚举但它只会在对象自己查找,不会检查原型链。
for..in
循环会遍历对象自身及原型链上可枚举属性、Object.keys
会遍历对象自身可枚举的属性。此外须要注意的是,这个循环只能获取到 key
,并不能直接获取对象中属性的值。
若是想要直接获取到属性的值可使用 ES6 新增的 for..of
进行遍历,前提是该对象已经定义了迭代器属性。
var arrList = [1, 2, 3]; for (let v of arrList) { console.log(v) } // 1 // 2 // 3
因为数组有内置的@@iterator,能够直接使用它:
var arrList = [1, 2, 3]; var it = arrList[Symbol.iterator](); it.next(); // {value: 1, done: false} it.next(); // {value: 2, done: false} it.next(); // {value: 3, done: false} it.next(); // {value: undefined, done: true}
注意: 引用像iterator这样的特殊对象的时候要使用符号名,经过
Symbol.iterator
来获取@@iterator内部的属性。
这里的迭代器执行了四遍才执行完,和java
的迭代器机制相似,每次迭代 next 的时候内部指针(虽然js中没有指针的概念)都会向前移动并返回对象属性列表的下一个值(须要注意遍历属性/值的顺序)。
因为普通对象建立的时候未实现迭代器(@@iterator),致使对象没法使用 for..of
。可是咱们能够手动在对象上实现自定义的迭代器。
var obj = { a: 1, b: 2 } Object.defineProperty(obj, Symbol.iterator, { enumerable: false, configurable: true, writable: false, value: function() { var that = this, idx = 0, keys = Object.keys(that); return { next: function() { return { value: that[keys[idx++]], done: (idx > keys.length) } } } } }) // 使用迭代器遍历 var it = obj[Symbol.iterator](); it.next() // {value: 1, done: false} it.next() // {value: 2, done: false} it.next() // {value: undefined, done: true} // 使用for..of遍历 for(let v of obj) { console.log(v) } // 1 // 2
这里的思路就是经过闭包产生迭代计数器,每次遍历返回列表的下一个值。咱们甚至能够自定义咱们想要的任何迭代器。须要注意的是Symblol
是不可枚举的。
对象是建立无序键值对的一种数据结构,能够经过字面量建立,也能够经过 new
关键字调用构造器函数建立,但通常使用字面量更常见,能够经过 .
语法和 []
来访问属性获取属性值。
属性能够经过数据描述符(默认建立方式)及访问描述符来进行操做,使用它们能够实现咱们想要的结构,如:禁止扩展、密封、冻结等操做。
属性不必定包含值,它们能够是经过setter
和 getter
来操做对象。
经过遍厉咱们了解到 enumerable
属性描述符的做用,介绍了 for..of
、Object.keys
及for..of
对遍历对象时各自的用途,其中经过for..of
咱们了解到能够为对象自定义迭代器来使用 for..of
遍历。