如何理解Object.defineProperty()?

幾乎全部使用Vue的開發者都知道,Vue的雙向綁定是經過Object.defineProperty()實現的,也知道在getter中收集依賴,在setter中通知更新。前端

那麼除了知道getter和setter以外,Object.defineProperty()還有哪些值得咱們去注意的地方呢?是否是有不少細節的東西不懂呢?git

你可能會說,除了getter和setter以外,Object.defineProperty()還有value,writable,enumerable,configurable。github

那麼問題來了?segmentfault

  • value或writable與getter,setter能夠共存嗎?與enumerable,configurable呢?
  • 歸納講下writable,enumerable,configurable分別是什麼意思?
  • enumerable在Object.keys()和for...in以及展開操做符...是如何表現的?
  • configurable會限制哪些屬性不可redefine?value會被限制嗎?會限制屬性的刪除嗎?
  • 經過obj.foo和Object.defineProperty(obj,foo)方式定義的屬性有何區別?
  • data descriptor、accessor descriptor、shared descriptor是什麼?

若是看了上面這些問題一臉懵逼,不要驚慌,咱們先來看一道很是直觀易懂的題目:數組

// 實現下面的邏輯
console.log(a+a+a); // 'abc'

題目看完了,帶着問題開始閱讀下面的內容吧。
若是能耐心看完的話對於我的的前端技術提高會很是大。
往近了說,不出意外上面這些問題所有能夠迎刃而解,對於a+a+a題目的題解也會理解更加透徹。
往遠了說,能夠去看懂Vue源碼相關的實現,以及看懂任何使用到Object.defineProperty()這個API的庫的源碼實現,甚至最後本身寫個小輪子。瀏覽器

  • 初識Object.defineProperty()
  • 語法安全

    • 參數
    • 返回值
  • Object.defineProperty()概覽微信

    • 基本知識點
    • data和accessor兩種描述符
    • 描述符必須是data, accessor之一,不能同時具備兩種特性
    • 如何區分data descriptor和accessor descriptor?
    • descriptor key概覽前端工程師

      - 共享descriptor key概覽
      - data descriptor key概覽
      - accessor descriptor key概覽
    • 牢記屬性不只僅是descriptor本身的屬性,還要考慮繼承屬性
    • 三個很基礎可是很好的例子函數

      - 默認descriptor:不可寫,不可枚舉,不可配置
      - 重用同一對象記憶上一次的value值
      - 凍結Object.prototype
  • Object.defineProperty()詳解

    • 建立一個property
    • 修改一個property

      • Writable attribute
      • Enumerable attribute

        • 知識點
        • 在for...in中如何表現?
        • 在Object.keys()中如何表現?
        • 在展開操做符...中如何表現?
        • 如何檢測屬性是否能夠枚舉?
      • Configurable attribute
    • 增長屬性和默認值
    • 自定義setter和getter
    • properties的繼承
  • 如何獲取屬性的descriptor?
  • 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

初識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)

參數

  • obj 須要定義屬性的對象
  • prop 須要定義或者修改的property的名字或者Symbol
  • descriptor 定義和修改的property的描述符

返回值

返回傳遞進函數的對象。

Object.defineProperty()概覽

  • 基本知識點
  • data和accessor兩種描述符
  • 描述符必須是data, accessor之一,不能同時具備兩種特性
  • 如何區分data descriptor和accessor descriptor?
  • descriptor key概覽

    • 共享descriptor key概覽
    • data descriptor key概覽
    • accessor descriptor key概覽
  • 牢記屬性不只僅是descriptor本身的屬性,還要考慮繼承屬性
  • 三個很基礎可是很好的例子

    • 默認descriptor:不可寫,不可枚舉,不可配置
    • 重用同一對象記憶上一次的value值
    • 凍結Object.prototype

基本知識點

  • Object.defineProperty()容許精準添加或者修改對象上的一個屬性。
  • 經過const obj = {};obj.foo = 1這種賦值方式增添的屬性,能夠經過for...in或者Object.keys枚舉,他的值可能發生改變,也可能被刪除。
  • Object.defineProperty()容許對對象屬性的默認方法作出改變。
  • 默認狀況下經過Object.defineProperty()是immutable(不可變的)。不能經過delete obj.foo刪除這個屬性。
  • Object.defineProperty()具備data和accessor兩種描述符,描述符生效時只能是其中之一,不能同時生效。
  • data和accessor兩種描述符都是object,dataDescriptor = {value, writable},accessorDescriptor={get(){}, set(){}}
  • data和accessor有各自獨有的key,它們也有共享的key。data accessor特有的key爲value和writable,accessor descriptor特有的key爲get和set。共享的key爲configurable和enumerable。
  • 若是descriptor沒有value, writable, get和set,會被當作一個data descriptor;若是同時有value或writable和get或set,異常會拋出。

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
  • 默認值爲false
  • 當且僅當屬性的描述符類型可能發生變化以及屬性描述符可能從對象上刪除和這個屬性相關聯
  • configurable爲false時,非data descriptor的屬性不能被重定義,也就是說除value和writable以外的屬性不能定義,並且特別要注意,value能夠隨意改,而writable僅能從true改成false。get(), set(), enumerable, configurable是都不能從新定義的。
  • 並且不能切換descriptor的類型:data descriptor和accessor descriptor
  • configurable 不只僅影響屬性的修改,還影響到了屬性的刪除。configurable爲false時delete obj.o失效

爲何configurable設置爲false時要這樣設計?

  • 提高對象屬性可控性
  • 提高安全性

這是由於get(), set(), enumerable, configurable是權限相關的屬性,爲了不引發沒必要要的bug。
不少庫的做者不容許本身修改這個屬性,讓它保持在一種可控的狀態,從而代碼按照本身的預期去運行。並且這樣作也更加安全。

enumerable
  • 默認值爲false
  • 當且僅當對象的屬性枚舉展現時會和這個屬性相關聯
data descriptor key概覽
value
  • 默認值爲undefined
  • 屬性相關聯的value
  • 能夠是任何JavaScript值 number,object,function等等
writable
  • 默認是false
  • 當且僅當經過賦值操做符賦值時會和這個屬性相關聯
accessor descriptor key概覽
get
  • 默認值爲undefined
  • 做爲屬性的getter服務於屬性,若是沒有getter的話,get爲undefined。
  • 當property被訪問時,這個函數會在不傳參的狀況下調用而後,並將this設置爲訪問屬性的對象(this因爲繼承可能不是定義屬性的對象。)
  • 返回值會做爲property的value。
set
  • 默認值爲undefined
  • 做爲屬性的setter服務於屬性,若是沒有setter的話,set爲undefined。
  • 當屬性從新賦值時,函數在傳遞一個參數的狀況下調用,並將這個集合設置爲屬性賦值的對象。

牢記屬性不只僅是descriptor本身的屬性,還要考慮繼承屬性

爲確保保留了這些默認值:

  • 能夠freeze Object.prototype
  • 或者Object.create(null)

三個很基礎可是很好的例子

默認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

  • 當一個屬性在對象中存在時,Object.defineProperty()能夠根據descriptor中的值和對象返回值的配置嘗試修改這個屬性。
  • 若是舊的descriptor有configurable屬性,而且設置爲false,意思是」不可配置「。

    • 意味着不能修改任意共享descriptor和accessor descriptor的屬性的值
    • 能夠重定義data descriptor:value任意變,writable只能從true變爲false(不能從false改成true)。
    • 並且不能切換descriptor的類型:data descriptor和accessor descriptor
    • 違反規則報錯:Cannot redefine property: xxx;符合規則和沒有修改屬性的話不報錯。
Writable attribute

當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 '#<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)檢測屬性是否可遍歷。
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的問題

這個例子展現了繼承帶來的問題:

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__的上一級建立新的屬性

如何獲取屬性的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() 外部變量
  • 解法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'

參考資料:
https://developer.mozilla.org...
https://developer.mozilla.org...
https://developer.mozilla.org...

期待和你們交流,共同進步,歡迎你們加入我建立的與前端開發密切相關的技術討論小組:

努力成爲優秀前端工程師!
相關文章
相關標籤/搜索