Open FrankKai opened 4 years ago
静态方法Object.defineProperty()会直接在一个对象上定义一个新的属性,或者修改对象上已经存在的属性,然后返回这个对象。
const obj = {};
Object.defineProperty(obj, 'prop', {
value: 42,
writable: true
});
console.log(obj); // {prop: 42}
obj.prop = 43; // {prop: 43}
Object.defineProperty(obj, prop, descriptor)
返回传递进函数的对象。
对象的属性descriptor描述符主要有两种:data descriptor和accessor descriptor。
数据描述符指的是value,writable,它可能是可写的,也可能是不可写的。
权限描述符指的是通过getter-setter函数get(),set()
对property的描述。
下面的代码会报错的原因破案了:只能是data,accessor 之一。
Object.defineProperty({this, 'a', {
value: 'a', // data descriptor
get(){
// access descriptor
}
})
// `Invalid property descriptor.Cannot both specify accessors and a value or writable attribue.`
data accessor特有的key为value和writable。 accessor descriptor特有的key为get和set。
// 典型的data descriptor
Object.defineProperty({this, 'a', {
value: 'a',
writable: false
})
// 典型的accessor descriptor
Object.defineProperty({this, 'a', {
get(){ ... }
set(){ ... }
})
默认情况下是通过Object.defineProperty()定义属性的。
delete obj.o
失效为什么configurable设置为false时要这样设计?
这是因为get(), set(), enumerable, configurable是权限相关的属性,为了避免引起不必要的bug。 很多库的作者不允许自己修改这个属性,让它保持在一种可控的状态,从而代码按照自己的预期去运行。而且这样做也更加安全。
为确保保留了这些默认值:
var obj = {};
var descriptor = Object.create(null); // no inherited properties
descriptor.value = 'static';
// not enumerable, not configurable, not writable as defaults
Object.defineProperty(obj, 'key', descriptor);
// being explicit
Object.defineProperty(obj, 'key', {
enumerable: false,
configurable: false,
writable: false,
value: 'static'
});
function withValue(value) {
var d = withValue.d || (
// 记忆住上一次的值
withValue.d = {
enumerable: false,
writable: false,
configurable: false,
value: value
}
);
// 避免重复赋值
if (d.value !== value) d.value = value;
return d;
}
Object.defineProperty(obj, 'key', withValue('static'));
Object.freeze(Object.prototype)
属性如果在对象上不存在的话,Object.defineProperty()会创建一个新的属性。 可以省略很多描述符中字段,并且输入这些字段的默认值。
// 创建对象
var o = {};
// 定义属性a并且传入data descriptor
Object.defineProperty(o, 'a', {
value: 37,
writable: true,
enumerable: true,
configurable: true,
})
// 定义属性b并且传入accessor descriptor
// 伪造value(好处是更细粒度的value控制):外部变量和get()
// 伪造writable(好处是更细粒度的writable控制):外部变量和set()
// 在这个例子中,o.b的值与bValue做了强关联。bValue是什么值,o.b就是什么值。除非o.b被重新定义
var bValue = 38;
Object.defineProperty(o, 'b', {
get() { return bValue },
set(newValue) { bValue = newVlaue },
enumerable: true,
configurable: true,
})
// 不可以同时混合定义两者
Object.defineProperty(o, 'conflict', {
value: 'a',
get() { return 'a' }
})
// 报错:Cannot both specify accessors and a value or writable
// 重新解读报错:Cannot both specify accessors descriptor and data descriptor(a value or writable)
当writable设置为false时,属性是不可写的,意味着无法重新赋值。
Cannot assign to read only property 'b' of object '#<Object>'
// 非严格模式不会报错,只是赋值失败
var o = {};
Object.defineProperty(o, 'a', {
value: 37,
writable: false
});
console.log(o.a); // logs 37
o.a = 25; // 不会报错
// (只会在strict mode报错,或者值没改变也不会报错)
console.log(o.a); // logs 37. 重新赋值没有生效
// 严格模式会报错 // strict mode (function() { 'use strict'; var o = {}; Object.defineProperty(o, 'b', { value: 2, writable: false }); o.b = 3; // 抛出Cannot assign to read only property 'b' of object '#
##### Enumerable attribute
- 知识点
- 在for...in中如何表现?
- 在Object.keys()中如何表现?
- 在展开操作符...中如何表现?
- 如何检测属性是否可以枚举?
###### 知识点
- enumerable属性定义了属性是否可以被Object.assign()或者spread(...) pick到。
- 对于非symbol的属性,它还会影响到for...in和Object.keys()对属性的pick。
- 可以用`obj.propertyIsEnumerable(prop)`检测属性是否可遍历。
```js
var o = {};
Object.defineProperty(o, 'a', {
value: 1,
enumerable: true
});
Object.defineProperty(o, 'b', {
value: 2,
enumerable: false
});
Object.defineProperty(o, 'c', {
value: 3, // enumerable默认为false
});
o.d = 4; // enumerable默认为true
Object.defineProperty(o, Symbol.for('e'), {
value: 5,
enumerable: true
});
Object.defineProperty(o, Symbol.for('f'), {
value: 6,
enumerable: false
});
只有'a'和'd'打印了出来。 enumerable为true的都能被解构出来,不包括Symbol。
for (var i in o) {
console.log(i); // 'a','d'
}
只有'a'和'd'被搜集到。 enumerable为true的都能被解构出来,不包括Symbol。
Object.keys(o); // ['a', 'd']
enumerable为true的都能被解构出来,包括Symbol。
var p = { ...o }
p.a // 1
p.b // undefined
p.c // undefined
p.d // 4
p[Symbol.for('e')] // 5
p[Symbol.for('f')] // undefined
可以用obj.propertyIsEnumerable(prop)检测属性是否可遍历
o.propertyIsEnumerable('a'); // true
o.propertyIsEnumerable('b'); // false
o.propertyIsEnumerable('c'); // false
o.propertyIsEnumerable('d'); // true
o.propertyIsEnumerable(Symbol.for('e')); // true
o.propertyIsEnumerable(Symbol.for('f')); // false
configurable属性控制属性是否可以被修改(除value和writable外),或者属性被删除。
var o = {};
Object.defineProperty(o, 'a', {
get() { return 1; },
configurable: false
});
Object.defineProperty(o, 'a', {
configurable: true
}); // throws a TypeError
Object.defineProperty(o, 'a', {
enumerable: true
}); // throws a TypeError
Object.defineProperty(o, 'a', {
set() {}
}); // throws a TypeError (set初始值为undefined)
Object.defineProperty(o, 'a', {
get() { return 1; }
}); // throws a TypeError
// (即使set没有变化)
Object.defineProperty(o, 'a', {
value: 12
}); // throws a TypeError // ('value' can be changed when 'configurable' is false but not in this case due to 'get' accessor)
console.log(o.a); // logs 1
delete o.a; // 不能删除
console.log(o.a); // logs 1
属性的默认值很值得思考一下。 通过点操作符.赋值和通过Object.defineProperty()是有区别的。
两种赋初始值方式的区别如下
通过点操作符定义的属性等价于Object.defineProperty的data descriptor和共享descriptor为true。
var o = {};
o.a = 1;
// 等价于
Object.defineProperty(o, 'a', {
value: 1,
writable: true,
configurable: true,
enumerable: true
});
Object.defineProperty(o, 'a', { value: 1 });
// 等价于
Object.defineProperty(o, 'a', {
value: 1,
writable: false,
configurable: false,
enumerable: false
});
下面的例子展示了如何实现一个自存档的对象。 当temperature属性设置后,archive数组会打印。
function Archiver() {
var temperature = null;
var archive = [];
Object.defineProperty(this, 'temperature', {
get(){
console.log('get!');
return temperature;
},
set(value) {
temperature = value;
archive.push({ val: temperature });
}
});
this.getArchive = function(){ return archive; };
}
var arc = new Archiver();
arc.temperature; // 'get'
arc.temperature = 11;
arc.temperature = 13;
arc.getArchive(); // [{val: 11}, {vale: 13}]
var pattern = {
get() {
return 'I always return this string, ' +
'whatever you have assigned';
},
set() {
this.myname = 'this is my name string';
}
};
function TestDefineSetAndGet() { Object.defineProperty(this, 'myproperty', pattern); }
var instance = new TestDefineSetAndGet(); instance.myproperty = 'test'; console.log(instance.myproperty); // I always return this string, whatever you have assigned
console.log(instance.myname); // this is my name string
#### properties的继承
- 如果一个accessor属性是继承的,它的get和set方法会在属性被访问时调用,并且在后代对象上被修改。如果这些方法使用变量来存储这个值,这个值会在所有对象间共享(即使使用new仍然会共享)
- 如果一个value属性是继承的,它可以直接设置在对象上。但是,如果继承了一个不可写的值属性,它仍然会阻止修改对象上的属性。
主要为以下3个问题:
- Object.defineProperty与prototype的问题
- 如何解决 Object.defineProperty与prototype的问题
- Object.defineProperty的writable和__proto__
##### Object.defineProperty与prototype的问题
这个例子展示了继承带来的问题:
```js
function myclass() {
}
var value;
Object.defineProperty(myclass.prototype, "x", {
get() {
return value;
},
set(x) {
value = x;
}
});
var a = new myclass();
var b = new myclass();
a.x = 1;
console.log(b.x); // 1
如何解决这个问题呢? 可以将值存储在另一个this属性上。这样使用new创建新实例时,可以为自己开辟单独的属性空间。 在get和set方法中,this指向使用、访问、修改属性的对象实例。
function myclass() {
}
Object.defineProperty(myclass.prototype, "x", {
get() {
return this._x;
},
set(x) {
this._x = x; // 用this._x来存储value
}
});
var a = new myclass();
var b = new myclass();
a.x = 1;
console.log(b.x); // 1
下面的例子,点操作符赋值的属性可写,但是继承的myclass.prototype的初始值不会发生更改;不可写的属性不可写。
function myclass() {
}
myclass.prototype.x = 1;
Object.defineProperty(myclass.prototype, "y", {
writable: false,
value: 1
});
var a = new myclass();
a.x = 2;
console.log(a.x); // 2
console.log(myclass.prototype.x); // 1
a.y = 2; // Ignored, throws in strict mode
console.log(a.y); // 1
console.log(myclass.prototype.y); // 1
值得分析一波的截图:
Object.getOwnPropertyDescriptor(obj,prop)
使用示例:
var o = {};
Object.defineProperty(o, 'a', { value: 1 });
Object.getOwnPropertyDescriptor(o,'a')
// {
// configurable: false
// enumerable: false
// value: 1
// writable: false
// }
/*
console.log(a + a + a); // 打印'abc'
*/
a...z
a...z
/**
* 解法1: Object.defineProperty() 外部变量
*/
let value = "a";
Object.defineProperty(this, "a", {
get() {
let result = value;
if (value === "a") {
value = "b";
} else if (value === "b") {
value = "c";
}
return result;
},
});
console.log(a + a + a);
/**
* 解法1(优化版):Object.defineProperty() 内部变量
*/
Object.defineProperty(this, "a", {
get() {
this._v = this._v || "a";
if (this._v === "a") {
this._v = "b";
return "a";
} else if (this._v === "b") {
this._v = "c";
return "b";
} else {
return this._v;
}
},
});
console.log(a + a + a);
/**
/**
/**
/**
/
题目扩展: 打印a...z
a+a+a; //'abc'
a+a+a+a; //'abcd'
/
/**
// 打印‘abcd’ let code = "a".charCodeAt(0); let count = 0; // {...定义a...} console.log(a + a + a); // 'abcd'
// 打印‘abcdefghijklmnopqrstuvwxyz’ let code = "a".charCodeAt(0); let count = 0; // {...定义a...} let str = ""; for (let i = 0; i < 27; i++) { str += a; } console.log(str); // "abcdefghijklmnopqrstuvwxyz"
/
题目扩展(优化版): 打印a...z
a+a+a; //'abc'
a+a+a+a; //'abcd'
/
Object.defineProperty(this, "a", { get() { this._code = this._code || "a".charCodeAt(0); let char = String.fromCharCode(this._code); if (this._code >= "a".charCodeAt(0) + 26) { return ""; } this._code++; return char; }, }); // 打印‘abc’ console.log(a + a + a); // 'abc'
参考资料: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/propertyIsEnumerable
几乎所有使用Vue的开发者都知道,Vue的双向绑定是通过Object.defineProperty()实现的,也知道在getter中收集依赖,在setter中通知更新。
那么除了知道getter和setter之外,Object.defineProperty()还有哪些值得我们去注意的地方呢?是不是有很多细节的东西不懂呢?
你可能会说,除了getter和setter之外,Object.defineProperty()还有value,writable,enumerable,configurable。
那么问题来了?
data descriptor、accessor descriptor、shared descriptor
是什么?如果看了上面这些问题一脸懵逼,不要惊慌,我们先来看一道非常直观易懂的题目:
题目看完了,带着问题开始阅读下面的内容吧。 如果能耐心看完的话对于个人的前端技术提升会非常大。 往近了说,不出意外上面这些问题全部可以迎刃而解,对于a+a+a题目的题解也会理解更加透彻。 往远了说,可以去看懂Vue源码相关的实现,以及看懂任何使用到Object.defineProperty()这个API的库的源码实现,甚至最后自己写个小轮子。
console.log(a+a+a); // 'abc'
题解a...z
a...z