一、第三章:对象
1.属性描述符(数据描述符)
可以使用 Object.getOwnPropertyDescriptor( myObject, "a" ); 获取myObject对象中属性a的属性描述符。
举个例子
var obj = {
a:1
}
console.log(Object.getOwnPropertyDescriptor(obj,'a'));
使用 Object.defineProperty( myObject, "a", { value: 2, writable: true, configurable: true, enumerable: true } )? 添加或修改a属性的属性描述符。
举个例子
var obj = {
a:1
}
// 修改a的属性描述符
Object.defineProperty(obj,'a',{
value:2,
configurable:true,
writable:false,
enumerable:false
});
console.log(Object.getOwnPropertyDescriptor(obj,'a'));
属性描述符(数据描述符)有三个可选值
● writable(可写的):表示是否可以修改值。如果不能修改,在严格模式下会报错(TypeError)
举个例子
var obj = {}
// 修改a的属性描述符
Object.defineProperty(obj,'a',{
value:1,
writable:false
})
console.log(obj.a); // 1
obj.a = 2;
console.log(obj.a); // 1(非严格模式) TypeError(严格模式下)
● configurable(可配置的):如果是true,则可以通过 Object.defineProperty() 修改三个属性描述符。
举个例子
var obj = {};
Object.defineProperty(obj,'a',{
value:1,
configurable:false
});
Object.defineProperty(obj,'a',{
configurable:true
}); // 直接TypeError
注意1:当设置不可配置的时候,可以将 writable 属性描述符由 true 设置为 false。反之不行(false => true)
举个例子
var obj = {
a:1
};
Object.defineProperty(obj,'a',{
configurable:false
});
obj.a = 2;
console.log(obj.a); // 2
Object.defineProperty(obj,'a',{
writable:false
}); // 不会报错,反而设置成功
obj.a = 3;
console.log(obj.a); // 2
注意2:configurable设置为 false 时,delete删除对象的属性也会失效(非严格),严格模式下不允许删除(TypeError)。
举个例子
var obj = {
a:1
};
console.log(obj.a); // 1
delete obj.a;
console.log(obj.a); // undefined
Object.defineProperty(obj,'a',{
value:1,
configurable:false
})
delete obj.a;
console.log(obj.a); // 1
● enumerable(可枚举的):如果false,则表示不能够被枚举(for循环遍历)。
举个例子
var obj = {
a:1
};
console.log(obj.a); // 1
Object.defineProperty(obj,'b',{
value:2,
enumerable:false
});
for(const key in obj) {
console.log(key); // a,没有b
}
2.不变性
如果希望对象的属性是不可改变的,就可以使用下面四种方式来实现。但是这些方法都是浅不变性,即目标对象引用了其他对象(如函数,对象,数组等),这些函数等是可以变化的。
(1) 对象常量
结合 writable:false 和 configurable:false 就可以实现一个常量属性(不可写、不可delete、不可重定义)
var obj = {};
Object.defineProperty(obj,'AAA',{
value:1,
writable:false,
configurable:false
});
console.log(obj.AAA); // 1
obj.AAA = 2; // 不可修改
console.log(obj.AAA); // 1
delete obj.AAA; // 不可删除
console.log(obj.AAA); // 1
Object.defineProperty(obj,'AAA',{
value:2
}); // TypeError 不可重定义(其中一个为true,都可重定义)
(2) 禁止扩展
禁止对象添加新属性,且保留已有属性,可以修改已有属性值,可以重新配置。(Object.preventExtensions())
var obj = {
a:1
};
Object.preventExtensions(obj)
console.log(Object.getOwnPropertyDescriptor(obj,'a')); // 三个属性描述符都为 true
obj.b = 2; // 非严格模式静默失败,严格模式 TypeError
console.log(obj.b); // undefined
(3) 密封
禁止添加新属性,禁止重新配置已有属性(writable从true到false除外),可以修改已有属性的值。(Object.seal())
Object.seal()的本质是Object.preventExtensions() + configurable:false
var obj = {
a:1
};
Object.seal(obj)
console.log(Object.getOwnPropertyDescriptor(obj,'a'));
// writable: true, enumerable: true, configurable: false
obj.b = 2; // 非严格模式静默失败,严格模式 TypeError
console.log(obj.b); // undefined
obj.a = 3; // 可以修改
console.log(obj.a); // 3
(4) 冻结(最高级别)
禁止添加新属性,禁止重新配置已有属性,禁止修改已有属性,只能读取值。(Object.freere())
Object.freeze()的本质是 Object.seal() + writable:false
var obj = {
a:1
};
Object.seal(obj)
console.log(Object.getOwnPropertyDescriptor(obj,'a'));
// writable: false, enumerable: true, configurable: false
obj.b = 2; // 非严格模式静默失败,严格模式 TypeError
console.log(obj.b); // undefined
obj.a = 3; // 不可以修改
console.log(obj.a); // 1
3.[[Get]]:获取属性值,[[Put]]:给属性赋值
这两个是对象默认的操作,分别用于获取属性值和给属性赋值。
4.Getter与Setter
可以使用 Getter 和 Setter 部分改写单个属性的默认操作([[Get]]、[[Set]])。Getter和Setter是两个隐藏的函数,当获取属性值和给属性赋值时被调用。
var obj = {
get a(){
return 1
}
};
Object.defineProperty(obj,'b',{
get(){
return this.a*2;
}
});
obj.a = 10;
console.log(obj.a); // 1
console.log(obj.b); // 2
var obj = {};
Object.defineProperty(obj,'a',{
get(){
return this.key;
},
set(val){
this.key = val * 2;
}
});
obj.a = 10; // 赋值触发set
console.log(obj.a); // 20 获取值触发get
var obj = {
get a(){
return this.key
},
set a(val){
this.key = val * 2;
}
};
obj.a = 10;
console.log(obj.a); // 20
5.存在性
在不访问属性值的情况下判断这个对象是否具有某个属性。
方法一:key in obj; 会查找原型链。
方法二:obj.hasOwnProperty('key'); 不会查找原型链,检查当前对象中是否存在。
// 假设obj原型上也找不到b
var obj = {
a:1
};
console.log(a in obj); // true
console.log(b in obj); // false
console.log(obj.hasOwnProperty('a')); // true
console.log(obj.hasOwnProperty('b')); // false
● 枚举
当属性描述符 enumerable 为 false 时,就表示不可枚举,即在循环中不可遍历。
var obj = {
a:1
}
Object.defineProperty(obj,'b',{
value:2,
enumerable:true
})
Object.defineProperty(obj,'c',{
value:2,
enumerable:false
})
for(var key in obj){
console.log(key); // a b
console.log(obj[key]); // 1 2
}
关于获取对象中 key 的方法
(1) Object.keys(obj):以数组的形式返回对象中可枚举的key(不会查找原型链)。
(1) Object.getOwnPropertyNames(obj):以数组的形式返回对象中的key(不会查找原型链,不论是否可枚举)。
var obj = {
a:1
}
Object.defineProperty(obj,'b',{
value:2,
enumerable:true
})
Object.defineProperty(obj,'c',{
value:2,
enumerable:false
})
console.log(Object.keys(obj)); // [a,b]
console.log(Object.getOwnPropertyNames(obj)); // [a,b,c]
6.遍历
for...in 循环遍历的是对象的 key,和数组的下标,这样有局限性。ES6新增的 for...of 循环可以直接遍历数组的值。
var arr = ['a','b','c'];
for (var i in arr) {
console.log(i); // 0 1 2
console.log(arr[i]); // a b c
}
for (var v of arr) {
console.log(v); // a b c
}
只有实现了迭代器的数据类型(如数组)才可以使用 for...of 循环遍历,js默认实现了迭代器的数据类型有 Array、String、Map、Arguments、Set、Nodelist。这些都可以使用 for...of 遍历。
可以使用 Symbol.iterator 来获取对象的内部属性@@iterator,@@iterator是一个返回迭代器对象的函数,调用它即可获取当前对象的迭代器。
举个例子
var arr = ['a','b','c'];
var a = arr[Symbol.iterator]();
console.log(a);
console.log(a.next()); // {value:'a',done:false}
console.log(a.next()); // {value:'b',done:false}
console.log(a.next()); // {value:'c',done:false}
console.log(a.next()); // {value:undefined,done:true}
console.log(a.next()); // {value:undefined,done:true}
但是普通 Object 并没有默认实现迭代器,官方解释是说怕影响未来的对象类型。我们可以使用自己手写创建迭代器来实现普通对象的 for...of 遍历。
var obj = {
a:1,b:2
};
// 定义obj普通对象的迭代器
Object.defineProperty(obj,Symbol.iterator,{
configurable:true,
writable:false,
enumerable:false,
value:function(){
// 保存当前对象 (obj)
var that = this;
// 获取obj的所有可枚举属性,key是数组
var key = Object.keys(that)
var index = 0;
return {
next:function(){
return{
// 返回值
value:that[key[index++]],
done:(index > key.length),
};
}
};
}
})
var o = obj[Symbol.iterator]();
console.log(o.next());
console.log(o.next());
console.log(o.next());
for(var v of obj){
console.log(v);
}
?二、原型 [[Prototype]]
在JavaScript每个对象中都有这样一个属性 [[Prototype]] ,它的名字叫做原型,表示对象之间的关联关系。当查找对象中的某个属性或方法时,会沿着 [[Prototype]] 进行查找。
1.属性设置和屏蔽
给对象设置属性的详细过程(如给obj对象设置name属性? obj.name = '小媛')
(1) 如果name属性在obj对象中存在,显而易见直接修改值即可。
(2) 如果在obj对象中不存在,就会查找原型链。当查询至原型链尽头仍未找到,则在obj中创建并赋值。
(3) 当在obj中不存在,而obj的原型链中存在,就会有以下三种情况。
● 如果原型链中的 name 属性是可写的(writable:true),则在obj中创建并赋值属性name(相当于屏蔽了UpObj中的name)。
// 设置obj原型链中的对象
var UpObj = { name:'小明' };
// 使obj的原型指向UpObj
var obj = Object.create(UpObj);
console.log(obj.name); // '小明'
obj.name = '小媛';
console.log(obj.name); // '小媛'
● 如果原型链中的 name 属性是不可写的(writeable:false),则默认失败(非严格)或TypeError)(严格模式)。
var UpObj = {};
Object.defineProperty(UpObj,'name',{
value:'小明',
writable:false
})
// 使obj的原型指向UpObj
var obj = Object.create(UpObj);
console.log(obj.name); // '小明'
obj.name = '小媛'; // 严格模式下这行代码TypeError错误
console.log(obj.name); // '小明'
如果使用 Object.defineProperty()设置obj的name则会成功。(相当于obj将会屏蔽UpObj中的name)
var UpObj = {};
Object.defineProperty(UpObj,'name',{
value:'小明',
writable:false
})
// 使obj的原型指向UpObj
var obj = Object.create(UpObj);
console.log(obj.name); // '小明'
// obj.name = '小媛';
Object.defineProperty(obj,'name',{
value:'小媛'
})
console.log(obj.name); // '小媛'
● 如果原型链中的name是一个 setter,则必定会调用这个 setter 并且不会将 name 属性添加到obj中。
当然使用?Object.defineProperty() 同上。
隐式屏蔽
指在不经意间屏蔽了原型链上的相同属性。
var anotherObject = {
a:2
};
var myObject = Object.create( anotherObject );
anotherObject.a; // 2
myObject.a; // 2
anotherObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "a" ); // false
myObject.a++; // 隐式屏蔽!
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty( "a" ); // true
myObject.a++ 相当于 myObject.a = myObject.a + 1
2.函数中 new 关键字
所有函数都有一个不可枚举的 prototype 属性,注意这个属性与 [[Prototype]] 不同,虽然他们都是指向某一个对象。函数的 prototype 属性指向的对象中有一个不可枚举的 constructor 属性,它指向函数本身。
function f(){}
console.dir(f);
console.log(f.prototype.constructor === f); // true
当使用 new 关键字调用函数时,会返回一个对象,这个对象的原型链([[Prototype]])中有一个constructor 属性指向被 new 调用的函数。
function f(){}
console.dir(f);
var a = new f();
console.log(a);
console.log(a.constructor === f.prototype.constructor); // true
console.log(a.constructor === f); // true
console.log(f.prototype.constructor === f); // true
把 constructor 指向 f 函数当成 a 对象是由 f 函数“构造”的,看似比较容易理解,其实这是一个误区。?a.constructor 只是通过 [[Prototype]] 默认的委托指向 f 函数,这与构造毫无关系。并且 constructor 属性是不可被信赖的,它很容易被修改。
function f(){}
f.prototype.constructor = 123;
console.dir(f)
var a = new f();
console.log(a);
console.log(a.constructor === f.prototype.constructor); // true
console.log(a.constructor === f); // false
console.log(f.prototype.constructor === f); // false
3.原型继承
JavaScript的原型继承只是通过原型 [[Prototype]] 关联,而不是真正的继承。
function Foo(name) {
this.name = name;
}
Foo.prototype.myName = function () {
return this.name;
};
function Bar(name, label) {
Foo.call(this, name);
this.label = label;
}
// 我们创建了一个新的 Bar.prototype 对象并关联到 Foo.prototype
Bar.prototype = Object.create(Foo.prototype);
// 注意!现在没有 Bar.prototype.constructor 了
// 如果你需要这个属性的话可能需要手动修复一下它
Bar.prototype.myLabel = function () {
return this.label;
};
var a = new Bar("a", "obj a");
console.log(a.myName()); // "a"
console.log(a.myLabel()); // "obj a"
Bar.prototype = Object.create(Foo.prototype); 这行代码的含义是创建一个名为 Bar.prototype 的对象(其实就是Bar中的prototype属性指向这个对象),对象的原型 [[Prototype]] 指向 Foo.prototype。这样就可以将 Foo 函数和 Bar 函数的通过 [[Prototype]] 原型链关联起来。使对象 a 可以访问到 myName属性。
由于Bar.prototype 更改了指向,那么原来所指向的对象会被销毁(垃圾回收器回收)。
如何才能在不销毁 Bar.prototype 旧对象的前提下修改指向。使之同样具有?Bar.prototype = Object.create(Foo.prototype); 这行代码的功能?
● 方法一(ES6之前的做法):使用 __proto__直接修改原型 [[Prototype]]
Bar.prototype.__proto__ = Foo.prototype;
缺点:__proto__不是标准并且不能兼容所有浏览器。
●?方法二(ES6新方法):Object.setPrototypeOf(a,b);将a的[[Prototype]]指向b
Object.setPrototypeOf(Bar.prototype , Foo.prototype);
var a = {num:1};
var b = {name:'77'};
Object.setPrototypeOf(a,b);
console.log(a.name); // 77
4.检查一个实例的继承祖先
判断一个实例(JS中的对象)是否是某个函数的继承祖先。即判断两个对象是否通过原型链联系。不严谨可以说:如何判断一个实例是否是某个函数构造的。
● a?instanceof Foo:只能用于左侧对象与右侧函数,表示 Foo.prototype 是否在 a 对象的原型链上。
缺点:只能判断"父子",不能判断"爷孙"
function Foo(){};
var a = new Foo();
// a是否是Foo的实例?Foo.prototype是否出现在a的原型链上?
console.log(a instanceof Foo); // true
● a.isPrototypeOf(b):表示 a 是否出现在 b 的原型链上。既能判断"父子",又能判断"爷孙",能判断整个原型链
function Foo(){};
var a = new Foo();
// Foo是否出现在a的原型链上?
console.log(Foo.isPrototypeOf(a)); // false
// Foo.prototype是否出现在a的原型链上
console.log(Foo.prototype.isPrototypeOf(a)); // true
此方法可以用于直接判断两个对象
var a = {};
var b = Object.create(a);
// 对象a是否在b的原型链上?
console.log(a.isPrototypeOf(b)); // true
console.log(b.isPrototypeOf(a)); // false
● 可以使用?Object.getPrototypeOf( a );获取a的原型链,与Foo.prototype比较。
缺点:只能判断"父子",不能判断"爷孙"
function Foo(){};
var a = new Foo();
console.log(Object.getPrototypeOf(a) === Foo.prototype) //true
// 只能判断父子,不能判断"爷孙"
console.log(Object.getPrototypeOf(a) === Object.prototype) //false
// 可以判断"爷孙"
console.log(Object.prototype.isPrototypeOf(a)); // true
5.使用 Object.create(null) 创建一个不受原型链影响的对象,可以用来保存数据。
var a = Object.create(null)
console.log(a);
var a = Object.create(null)
a.num = 1;
console.log(a);
?
|