一、属性描述符
在es5之前,javaScript本身是没法直接检测属性特性的,比如判断属性是否是只读。
es5开始,有了属性描述符,在创建普通属性时属性描述符使用默认值。
var myObject = {
a:2
};
Object.getOwnPropertyDescripter(myObject, "a");
//{
// value: 2,
// writable: true, 可写性
// enmerable: true, 可枚举性
// configurable: true 可配置性
//}
我们也可以自己设置,通过Object.defineProperty()方法(vue源码中的数据绑定也是通过这个方法外加观察者模式实现的)
var myObject = {}
Object.defineProperty(myObject, "a", {
value: 2,
writable: true,
configurable: true,
enumerable: true,
// get: function(){ getter和setter不能和value或writable连用
// return this._a_*2;
// },
// set: function(val){
// this._a_ = val;
// _a_代表一个普通变量,给a赋值时调用setter,触发对它的更改,直接写a的话,会发生栈溢出(而且逻辑也不对)
// }
});
myObject.a // 2
下面来介绍下这里的属性~
Writable
决定是否可修改属性的值,设为false的话,属性值不可更改,如果在严格模式下更改,会抛出错误。设为false后,还会禁止删除这个属性
Configurable
决定属性是否可配置,设为false的话,不可再用Object.defineProperty改变它的状态,会报错。
所以把它修改成false是单向操作。
(但有一个小例外,即使把它设为false,我们还是可以把writable由true变为false,但不能反向改)
Enumerable
控制属性是否会出现在对象的属性枚举中,比如for···in循环,设为false后,属性就不会出现在枚举中,但仍然可以正常访问它。
Getter和Setter
这俩只能应用在单个属性上,无法应用在整个对象上(或许以后会有办法)。
getter在获取属性值时调用,setter在设置属性值时调用。
这俩还有另一种写法
var myObject = {
get a(){
return this._a_;
},
set a(val){
this._a_ = val;
}
};
接下来说一下,设置属性值时都发生了什么 (其实这是个略微复杂的过程,并不像看到的那么简单)
首先,如果这个属性已存在对象本身内部
看属性是否是setter,是就调用,不是的话判断属性writable是否为false,为false的话严格模式下会报错,非严格模式静默失败,不为false的话将值设为属性值。
如果属性存在于对象原型链上
如果属性writable为true的话,在对象内部添加同名的属性并赋值,如果为false,赋值语句会被忽略,严格模式下报错。如果属性是setter,那就一定会调用这个setter,并且不会在对象内部添加同名属性。
如果非要在对象内部添加同名属性,可用Object.defineProperty实现。
二、保持对象不变
禁止扩展
可以通过Object.preventExtensions()方法,禁止一个对象添加新方法并且保留已有属性。
var myObject = {
a:2
}
Object.preventExtensions(myObject)
myObject.b = 3;
myObject.b // undefined
密封
Object.seal()密封对象,不仅不能添加新属性,也不能重新配置或删除任何现有属性(但可以修改属性值)
实际上就是在对象上调用Object.preventExtensions()并把所有属性的configurable设为false。
冻结
object.freeze()冻结对象,是应用在对象上最高级的不可变性,禁止对于对象本身及其任意直接属性的修改(对象内的属性如果也是对象的话,只可以保证这个引用不变,但改变这个属性对应的对象内部的东西它监测不到,除非遍历整个对象,不断调用freeze方法,实现深度冻结)
实际上是在对象上调用seal方法,并把所有“数据访问”属性标记为writable:false
(楼主面试的时候被问到过,自己封装一个freeze方法)
三、遍历
遍历对象,相信大家最常用的方法是for···in循环吧,它会遍历对象的可枚举属性,当然,数组也可以用它遍历,它会遍历数组的下标。
所以,不难猜出
for···in循环实际上遍历的是对象的属性名,而不是属性值。
还有一种循环可以直接遍历属性值,es6新增的for···of循环
举个例子
let arr = [1,2,3];
for(let i in arr){
console.log(i);
} //0,1,2 遍历的是数组下标
for(let i of arr){
console.log(i);
}//1,2,3 遍历的是值
这样大家可以看出区别了吧
但还有一个问题
for···of循环的原理是向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的next方法实现遍历
(说白了,就是访问iterator接口,这也是es6提出的,有这个接口的可以直接用for···of循环,没有的不可以,像数组,字符串是天生就有的,但对象没有,所以对象不能直接用for···of循环,类数组大部分没有,但有特例,这部分以后写es6时会细说一下,es6的魅力还是很大的~)
我们可以手动访问这个接口,看看工作原理
let arr = [1,2,3];
let it = arr[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 }
就是这样的,每次调用next来遍历,返回value和done表示是否遍历完成,具体的以后写再说吧(不然太多了~)
虽然对象没有iterator接口,但活人总不能被尿憋死,我们可以自己给它加,这样就可以使用for···of循环了~
Object.defineProperty(myObject, Symbol.iterator, {
enumerable: false,
writable:false,
configurable:false,
value:function(){
let o = this;
let idx = 0;
let ks = Object.keys(o);
return {
next:function(){
return {
value:o[ks[idx++]],
done:(idx>ks.length)
}
}
}
}
});
这又是楼主当时的一道面试题,给对象加iterator接口,跟上面那个问题是一个公司问的,当时只是大概的实现了下,没写这么细,但感觉这家公司问的问题都挺有意思的,也挺有代表性,并且也在我最心仪的城市,所以最后决定去了这家公司,希望决定是对的吧,好了,今天先到这吧~