本篇文章需要对 JavaScript 的原型链属性描述符有一定的了解。

本篇文章主要分析一下,在 JavaScript 给对象设置属性或是修改对象的属性值背后的过程。

首先看这样一段代码,熟悉一下原型链:

1
2
3
4
5
6
7
8
var obj = {}
obj.a = 1
console.log(obj.a) // 1

var obj2 = {}
console.log(obj2.__proto__ === Object.prototype) // true
Object.prototype.a = 233
console.log(obj2.a) // 233

我们分析一下:

  1. obj1 没什么好说的,给当前对象上设置一个属性 a,然后读取该值。
  2. obj2 虽然没有属性 a,但是 js 会在 obj2 的原型链上查找 a 属性,然后输出 233。

属性存在于原型链上而不存在于要赋值对象的三种情况

第一种情况:

如果在**[[prototype]]**链上存在同名属性,并且没有被标记为只读 writable:false,创建屏蔽属性。

我们思考一下,如果有一个 obj3 原型链上有一个属性 foo ,然后仍然我们给 obj3 设置一个同名属性 foo,会发生什么?
实践一下:

1
2
3
4
5
var obj3 = {}
Object.prototype.foo = 'i am prototype foo'
obj3.foo = 'i am obj3 foo'
console.log(obj3.foo) // i am obj3 foo
console.log(Object.prototype.foo) // i am prototype foo

这样的输出是符合预期的,我们给 obj3 添加了一个 foo 属性,而不是修改 Object.prototype.foo 的值。

这种行为称之为属性屏蔽,obj3 的 foo 属性屏蔽了原型链上层的 foo 属性。

第二种情况:

如果在**[[prototype]]**链上存在同名属性,并且被被标记为只读 writable:false

  • 非严格模式下:无法创建屏蔽属性,也无法修改链上的属性。
  • 严格模式下,报错。

非严格模式:

1
2
3
4
5
6
7
8
var obj3 = {}
Object.defineProperty(Object.prototype, 'foo', {
writable: false,
value: 333
})
obj3.foo = 'i am obj3 foo' // 该语句被忽略
console.log(obj3.foo) // 333
console.log(Object.prototype.foo) // 333

严格模式:

1
2
3
4
5
6
7
8
9
'use strict'
var obj3 = {}
Object.defineProperty(Object.prototype, 'foo', {
writable: false,
value: 333
})
obj3.foo = 'i am obj3 foo' // TypeError: Cannot assign to read only property 'foo' of object '#<Object>'
console.log(obj3.foo)
console.log(Object.prototype.foo)

第三种情况:

如果在**[[prototype]]链上存在同名属性,并且它是一个[[setter]]**,那么就会调用它。

1
2
3
4
5
6
7
8
9
var obj3 = {}
Object.defineProperty(Object.prototype, 'foo', {
set: function () {
console.log('i am a setter!')
}
})
obj3.foo = 'i am obj3 foo' // i am a setter!
console.log(obj3.foo) // undefined
console.log(Object.prototype.foo) // undefined

可以到执行 obj3.foo 时,输出了 i am a setter! ,并且也没有发生属性屏蔽

可以看到,js 中设置一个属性并没有那个简单,除了第一种情况,剩下两种情况的行为都比较 “奇怪”,需要注意。

那么我们如何让 属性屏蔽 总是发生?

使用Object.defineProperty来屏蔽属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 第二种情况
var obj = {}
Object.defineProperty(Object.prototype, 'foo1', {
writable: false,
value: 'Object.prototype.foo1'
})
Object.defineProperty(obj, 'foo1', {
value: 'obj.foo1'
})
console.log(obj.foo1) // obj.foo1
console.log(Object.prototype.foo1) // Object.prototype.foo1

// 第三种情况
var obj2 = {}
Object.defineProperty(Object.prototype, 'foo2', {
set: function () {
console.log('i am foo2 setter')
},
get: function () {
return 'get foo2 success!'
}
})
Object.defineProperty(obj2, 'foo2', {
value: 'obj.foo2'
})
console.log(obj2.foo2) // obj.foo2
console.log(Object.prototype.foo2) // get foo2 success!

可以看到,第二种和第三种情况都成功屏蔽了原型链上的同名属性!

参考链接: