如何理解Object.defineProperty()?

2020-04-30  本文已影响0人  趁你还年轻233

几乎所有使用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()

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

参数

返回值

返回传递进函数的对象。

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
set

牢记属性不仅仅是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)

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

Writable attribute

当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
}());
Enumerable attribute
知识点
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()是有区别的。

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

通过点操作符定义的属性

通过点操作符定义的属性等价于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使用方式
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的继承

主要为以下3个问题:

Object.defineProperty与prototype的问题

这个例子展示了继承带来的问题:

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

如何获取属性的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
// }

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

/*
  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'

参考资料:
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

期待和大家交流,共同进步,欢迎大家加入我创建的与前端开发密切相关的技术讨论小组:

image

努力成为优秀前端工程师!

上一篇 下一篇

猜你喜欢

热点阅读