Proxy 與 Object.defineProperty的對比(高頻率面試題)

前言

Object.defineProperty() 和 ES2015 中新增的 Proxy 對象,會經常用來做數據劫持(數據劫持:在訪問或者修改對象的某個屬性時,通過一段代碼攔截這個行為,進行額外的操作或者修改返回結果),數據劫持的典型應用就是我們經常在面試中遇到的雙向數據綁定。

Object.defineProperty

Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 并返回這個對象
語法:

Object.defineProperty(obj, prop, descriptor)

  • obj:要在其上定義屬性的對象。
  • prop:要定義或修改的屬性的名稱。
  • descriptor:將被定義或修改的屬性描述符。
var o = {}; // 創建一個新對象
// 在對象中添加一個屬性與數據描述符的示例
Object.defineProperty(o, "a", {
  value : 37,
  writable : true,
  enumerable : true,
  configurable : true
});
// 對象o中有一個值為37的屬性a

MDN這樣描述屬性描述符:
數據描述符和存取描述符。數據描述符是一個具有值的屬性,該值可能是可寫的,也可能不是可寫的。存取描述符是由getter-setter函數對描述的屬性。描述符必須是這兩種形式之一;不能同時是兩者。兩個屬性描述符的具體介紹可以查看MDN,這里不再綴訴。
示例:

// 正確
Object.defineProperty({}, "a", {
    value: 37,
    writable: true,
    enumerable: true,
    configurable: true
});
// 正確
var value = 37;
Object.defineProperty({}, "a", {
    get : function(){
      return value;
    },
    set : function(newValue){
      value = newValue;
    },
    enumerable : true,
    configurable : true
});

// 報錯
Object.defineProperty({}, "a", {
    value: 37,
    get: function() {
        return 1;
    }
});

Setters 和 Getters

下面的例子展示了如何實現一個自存檔對象。 當設置temperature 屬性時,archive 數組會獲取日志條目

function Archiver() {
  var temperature = null;
  var archive = [];

  Object.defineProperty(this, 'temperature', {
    get: function() {
      console.log('get!');
      return temperature;
    },
    set: function(value) {
      console.log('set!');
      temperature = value;
      archive.push({ val: temperature });
    }
  });

  this.getArchive = function() { return archive; };
}

var arc = new Archiver();
arc.temperature; // 'get!'
arc.temperature = 11;  // 'set!'
arc.temperature = 13;  // 'set!'
arc.getArchive(); // [{ val: 11 }, { val: 13 }]

存在對問題

一、不能監聽數組的變化
數組的以下幾個方法不會觸發 set,push、pop、shift、unshift、splice、sort、reverse

let arr = [1,2,3]
let obj = {}
Object.defineProperty(obj, 'arr', {
  get () {
    console.log('get arr')
    return arr
  },
  set (newVal) {
    console.log('set', newVal)
    arr = newVal
  }
})
obj.arr.push(4) // 只會打印 get arr, 不會打印 set
obj.arr = [1,2,3,4] // 這個能正常 set

二、必須遍歷對象的每個屬性
使用 Object.defineProperty() 多數要配合 Object.keys() 和遍歷,于是多了一層嵌套

Object.keys(obj).forEach(key => {
  Object.defineProperty(obj, key, {
    // ...
  })
})

三、必須深層遍歷嵌套的對象
如果嵌套對象,那就必須逐層遍歷,直到把每個對象的每個屬性都調用 Object.defineProperty() 為止。 Vue 的源碼中就能找到這樣的邏輯 (叫做 walk 方法)。

Proxy

Proxy 對象用于定義基本操作的自定義行為(如屬性查找,賦值,枚舉,函數調用等),ES6 原生提供 Proxy 構造函數,用來生成 一個Proxy 實例。
語法:

let p = new Proxy(target, handler);

  • target:用Proxy包裝的目標對象(可以是任何類型的對象,包括原生數組,函數,甚至另一個代理)。
  • handler:一個對象,其屬性是當執行一個操作時定義代理的行為的函數
    示例:
        let target = {};
        let handler = {
            get: function (obj, name) {
                console.log('get')
                return name in obj ? obj[name] : 37;
            },
            set: function (obj, name, value) {
                console.log('set');
                obj[name] = value
            },
        };
        let p = new Proxy(target, handler);
        p.a = 1;  // 進行set操作,并且操作會被轉發到目標
        p.b = undefined; // 進行set操作,并且操作會被轉發到目標
        console.log(p.a, p.b);    // 1, undefined ,進行get操作
        console.log('c' in p, p.c);    // false, 37  進行get操作
        console.log(target) // {a: 1, b: undefined}. 操作已經被正確地轉發

在例子中,通過new Proxy(target, handler)返回了一個Prosy實例,在訪問或者添加實例對象的某個屬性時
,調用了get或者set操作,在get操作中,在當對象不存在屬性名時,會返回37.除了進行get和set操作外,還會進行無操作轉發代理,代理會將所有應用到它的操作轉發到這個目標對象上。

解決問題

一、針對對象
Proxy 是針對 整個對象obj 的。因此無論 obj 內部包含多少個 key ,都可以走進 set。(并不需要通過Object.keys() 的遍歷),解決了上述 Object.defineProperty() 的第二個問題

let obj = {
  name: 'Eason',
  age: 30
}
let handler = {
  get (target, key, receiver) {
    console.log('get', key)
    return Reflect.get(target, key, receiver)
  },
  set (target, key, value, receiver) {
    console.log('set', key, value)
    return Reflect.set(target, key, value, receiver)
  }
}
let proxy = new Proxy(obj, handler)
proxy.name = 'Zoe' // set name Zoe
proxy.age = 18 // set age 18

Reflect.get 和 Reflect.set 可以理解為類繼承里的 super,即調用原來的方法

二、支持數組
Proxy 不需要對數組的方法進行重載,省去了眾多 hack,減少代碼量等于減少了維護成本,而且標準的就是最好的

let arr = [1,2,3]
let proxy = new Proxy(arr, {
    get (target, key, receiver) {
        console.log('get', key)
        return Reflect.get(target, key, receiver)
    },
    set (target, key, value, receiver) {
        console.log('set', key, value)
        return Reflect.set(target, key, value, receiver)
    }
})
proxy.push(4)
// 能夠打印出很多內容
// get push     (尋找 proxy.push 方法)
// get length   (獲取當前的 length)
// set 3 4      (設置 proxy[3] = 4)
// set length 4 (設置 proxy.length = 4)

三、嵌套支持
Proxy 也是不支持嵌套的,這點和 Object.defineProperty() 是一樣的。因此也需要通過逐層遍歷來解決。Proxy 的寫法是在 get 里面遞歸調用 Proxy 并返回

let obj = {
  info: {
    name: 'eason',
    blogs: ['webpack', 'babel', 'cache']
  }
}
let handler = {
  get (target, key, receiver) {
    console.log('get', key)
    // 遞歸創建并返回
    if (typeof target[key] === 'object' && target[key] !== null) {
      return new Proxy(target[key], handler)
    }
    return Reflect.get(target, key, receiver)
  },
  set (target, key, value, receiver) {
    console.log('set', key, value)
    return Reflect.set(target, key, value, receiver)
  }
}
let proxy = new Proxy(obj, handler)
// 以下兩句都能夠進入 set
proxy.info.name = 'Zoe'
proxy.info.blogs.push('proxy')

擴展

實現構造函數

方法代理可以輕松地通過一個新構造函數來擴展一個已有的構造函數,這個例子使用了construct和apply。

function extend(sup,base) {
  var descriptor = Object.getOwnPropertyDescriptor(
    base.prototype,"constructor"
  );
  base.prototype = Object.create(sup.prototype);
  var handler = {
    construct: function(target, args) {
      var obj = Object.create(base.prototype);
      this.apply(target,obj,args);
      return obj;
    },
    apply: function(target, that, args) {
      sup.apply(that,args);
      base.apply(that,args);
    }
  };
  var proxy = new Proxy(base,handler);
  descriptor.value = proxy;
  Object.defineProperty(base.prototype, "constructor", descriptor);
  return proxy;
}
var Person = function(name){
  this.name = name
};
var Boy = extend(Person, function(name, age) {
  this.age = age;
});
Boy.prototype.sex = "M";
var Peter = new Boy("Peter", 13);
console.log(Peter.sex);  // "M"
console.log(Peter.name); // "Peter"
console.log(Peter.age);  // 13

面試題

什么樣的 a 可以滿足 (a === 1 && a === 2 && a === 3) === true 呢?這里我們就可以采用數據劫持來實現

let current = 0
Object.defineProperty(window, 'a', {
  get () {
    current++
    return current
  }
})
console.log(a === 1 && a === 2 && a === 3) // true

總結

Proxy / Object.defineProperty 兩者的區別:

  • 當使用 defineProperty,我們修改原來的 obj 對象就可以觸發攔截,而使用 proxy,就必須修改代理對象,即 Proxy 的實例才可以觸發攔截
  • defineProperty 必須深層遍歷嵌套的對象。 Proxy 不需要對數組的方法進行重載,省去了眾多 hack,減少代碼量等于減少了維護成本,而且標準的就是最好的

Proxy對比defineProperty的優勢

  • Proxy 的第二個參數可以有 13 種攔截方法,這比起 Object.defineProperty() 要更加豐富
  • Proxy 作為新標準受到瀏覽器廠商的重點關注和性能優化,相比之下 Object.defineProperty() 是一個已有的老方法
  • Proxy 的兼容性不如 Object.defineProperty() (caniuse 的數據表明,QQ 瀏覽器和百度瀏覽器并不支持 Proxy,這對國內移動開發來說估計無法接受,但兩者都支持 Object.defineProperty())
  • 不能使用 polyfill 來處理兼容性

接下來我們將會分別用Proxy / Object.defineProperty 來實現雙向綁定

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容