几乎所有使用vue的开发者都知道,Vue的双向绑定是通过Object.defineProperty()实现的,也知道在getter中收集依赖,在setter中通知更新。那么除了知道getter和setter之外,Object.defineProperty()还有哪些值得我们去注意的地方呢?是不是有很多细节的东西不懂呢?
你可能会说,除了getter和setter之外,Object.defineProperty()还有value,writable,enumerable,configurable。
那么问题来了?
如果看了上面这些问题一脸懵逼,不要惊慌,我们先来看一道非常直观易懂的题目:
// 实现下面的逻辑
console.log(a+a+a); // 'abc'
题目看完了,带着问题开始阅读下面的内容吧。
如果能耐心看完的话对于个人的前端技术提升会非常大。
往近了说,不出意外上面这些问题全部可以迎刃而解,对于a+a+a题目的题解也会理解更加透彻。
往远了说,可以去看懂Vue源码相关的实现,以及看懂任何使用到Object.defineProperty()这个api的库的源码实现,甚至最后自己写个小轮子。
Object.defineProperty()概览
descriptor key概览
- 共享descriptor key概览
- data descriptor key概览
- accessor descriptor key概览
三个很基础但是很好的例子
- 默认descriptor:不可写,不可枚举,不可配置
- 重用同一对象记忆上一次的value值
- 冻结Object.prototype
Object.defineProperty()详解
修改一个property
Enumerable attribute
console.log(a+a+a); // 'abc'题解
静态方法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 key概览
三个很基础但是很好的例子
对象的属性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()定义属性的。
为什么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)
如果旧的descriptor有configurable属性,并且设置为false,意思是”不可配置“。
当writable设置为false时,属性是不可写的,意味着无法重新赋值。
// 非严格模式不会报错,只是赋值失败
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 '#<Object>'
return o.b; // 2
}());
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
主要为以下3个问题:
这个例子展示了继承带来的问题:
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'
*/
/**
* 解法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);
/**
* 解法2: Object.prototpye.valueOf()
*/
let index = 0;
let a = {
value: "a",
valueOf() {
return ["a", "b", "c"][index++];
},
};
console.log(a + a + a);
/**
* 解法3:charCodeAt,charFromCode
*/
let code = "a".charCodeAt(0);
let count = 0;
Object.defineProperty(this, "a", {
get() {
let char = String.fromCharCode(code + count);
count++;
return char;
},
});
console.log(a + a + a); // 'abc'
/**
* 解法3(优化版一):内部变量this._count和_code
*/
Object.defineProperty(this, "a", {
get() {
let _code = "a".charCodeAt(0);
this._count = this._count || 0;
let char = String.fromCharCode(_code + this._count);
this._count++;
return char;
},
});
console.log(a + a + a); // 'abc'
/**
* 解法3(优化版二):内部变量this._code
*/
Object.defineProperty(this, "a", {
get() {
this._code = this._code || "a".charCodeAt(0);
let char = String.fromCharCode(this._code);
this._code++;
return char;
},
});
console.log(a + a + a); // 'abc'
/*
题目扩展: 打印`a...z`
a+a+a; //'abc'
a+a+a+a; //'abcd'
*/
/**
* charCodeAt,charFromCode
*/
let code = "a".charCodeAt(0);
let count = 0;
Object.defineProperty(this, "a", {
get() {
let char = String.fromCharCode(code + count);
if (count >= 26) {
return "";
}
count++;
return char;
},
});
// 打印‘abc’
console.log(a + a + a); // 'abc'
// 打印‘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'
参考资料:
在JavaScript中的所有对象都是继承Object对象而来的, 我们可以理解为Object是所有js对象通用的功能,讲解Object的prototype属性、Object.prototype属性和方法?Object.prototype.constructor属性等
JavaScript中Object.create()的含义和语法,使用它创建null原型的对象 ,创建一个普通的空对象,Object.create()第二个参数说明。
相等是JavaScript中起初最让人困惑的部分。==和===的比较、强制类型的顺序等等,都使得这个问题变得复杂。今天,我们会聚焦另一个方面:object相等是如何实现的。
还能通过 Object.defineProperty() 方法,添加或修改对象的属性。更重要的是,除了目标对象 obj,属性名称 prop 外,方法能传入属性描述符 descriptor,以实现更复杂的性质。属性描述符是一个对象,有两种形式:一种是数据描述符,另一种是存取描述符。
Object的defineProperty和defineProperties这两个方法在js中的重要性十分重要,主要功能就是用来定义或修改这些内部属性,与之相对应的getOwnPropertyDescriptor和getOwnPropertyDescriptors就是获取这行内部属性的描述。
Object.defineProperty()该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象,Object.defineProperties()该方法直接在一个对象上定义新的属性或修改现有属性,并返回该对象
Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和使用 for...in 循环遍历该对象时返回的顺序一致 。
Object.freeze() 方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。
本文实例讲述了JS Object.preventExtensions(),Object.seal()与Object.freeze()用法。分享给大家供大家参考,Object.preventExtensions 只能阻止一个对象不能再添加新的自身属性,仍然可以为该对象的原型添加属性。
什么是浅拷贝?浅拷贝就是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。浅拷贝Object.assign()是什么?主要将所有可枚举属性的值从一个或者多个数据源对象复制到目标对象,同时返回目标对象。
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!