FrankKai / FrankKai.github.io

FE blog
https://frankkai.github.io/
362 stars 39 forks source link

如何理解Object.defineProperty()? #215

Open FrankKai opened 4 years ago

FrankKai commented 4 years ago

几乎所有使用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的库的源码实现,甚至最后自己写个小轮子。

FrankKai commented 4 years ago

初识Object.defineProperty()

静态方法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)

参数

FrankKai commented 4 years ago

Object.defineProperty()概览

data和accessor两种描述符

对象的属性descriptor描述符主要有两种:data descriptor和accessor descriptor。

data descriptor

数据描述符指的是value,writable,它可能是可写的,也可能是不可写的。

accessor descriptor

权限描述符指的是通过getter-setter函数get(),set()对property的描述。

描述符必须是data, accessor之一,不能同时具有两种特性

下面的代码会报错的原因破案了:只能是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 descriptor和accessor descriptor?

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(){ ... }
})

descriptor key概览

默认情况下是通过Object.defineProperty()定义属性的。

共享descriptor key概览
configurable

为什么configurable设置为false时要这样设计?

这是因为get(), set(), enumerable, configurable是权限相关的属性,为了避免引起不必要的bug。 很多库的作者不允许自己修改这个属性,让它保持在一种可控的状态,从而代码按照自己的预期去运行。而且这样做也更加安全。

enumerable
data descriptor key概览
value
writable
accessor descriptor key概览
get

牢记属性不仅仅是descriptor自己的属性,还要考虑继承属性

为确保保留了这些默认值:

三个很基础但是很好的例子

默认descriptor:不可写,不可枚举,不可配置
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'
});
重用同一对象记忆上一次的value值
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.prototype
Object.freeze(Object.prototype)
FrankKai commented 4 years ago

Object.defineProperty()详解

创建一个property

属性如果在对象上不存在的话,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)

修改一个property

// 严格模式会报错 // 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 '#' return o.b; // 2 }());

##### 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
});
在for...in中如何表现?

只有'a'和'd'打印了出来。 enumerable为true的都能被解构出来,不包括Symbol。

for (var i in o) {
  console.log(i); // 'a','d'
}
在Object.keys()中如何表现?

只有'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 attribute

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()是有区别的。

两种赋初始值方式的区别如下

  • 通过点操作符定义的属性,writable,configurable,enumerable值都为true,value为赋入的值
  • 通过Object.defineProperty只指定value的属性,writable,configurable,enumerable值都为false
通过点操作符定义的属性

通过点操作符定义的属性等价于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只指定value的属性
Object.defineProperty(o, 'a', { value: 1 });
// 等价于
Object.defineProperty(o, 'a', {
  value: 1,
  writable: false,
  configurable: false,
  enumerable: false
});

自定义setter和getter

下面的例子展示了如何实现一个自存档的对象。 当temperature属性设置后,archive数组会打印。

  • 常见的一种gettter,setter使用方式
  • 这个getter和setter总是返回相同的值
    常见的一种gettter,setter使用方式
    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}]
    这个getter和setter总是返回相同的值
    
    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
如何解决 Object.defineProperty与prototype的问题

如何解决这个问题呢? 可以将值存储在另一个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
Object.defineProperty的writable和proto

下面的例子,点操作符赋值的属性可写,但是继承的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

值得分析一波的截图: image

  • proto是通过Object.defineProperty(foo.prototype)实现的继承
  • 如果属性的writable为true,会在proto的上一级创建新的属性
FrankKai commented 4 years ago

如何获取属性的descriptor?

Object.getOwnPropertyDescriptor(obj,prop)

使用示例:

var o = {};
Object.defineProperty(o, 'a', { value: 1 });
Object.getOwnPropertyDescriptor(o,'a')
// {
//     configurable: false
//     enumerable: false
//     value: 1
//     writable: false
// }
FrankKai commented 4 years ago

console.log(a+a+a); // 'abc'题解

/*
  console.log(a + a + a); // 打印'abc'
*/
  • 解法1: Object.defineProperty() 外部变量
  • 解法1(优化版):Object.defineProperty() 内部变量
  • 解法2: Object.prototpye.valueOf()
  • 解法3:charCodeAt,charFromCode
  • 解法3(优化版一):内部变量this._count和_code
  • 解法3(优化版二):内部变量this._code
  • 题目扩展: 打印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);

/**

  • 解法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'