帶你重學ES6 | proxy和defineProperty

前言

雖然個人主技術棧是 React 的,可是每當面試的時候,面試官幾乎都會問你說一下 React 和 Vue 的區別,在說道雙向數據綁定的時候,面試官會下意識的問一句,你說一下 Vue 的雙向數據綁定的原理,這個時候 Object.defineProperty 就出場了,可是在 Vue3.0 中,Proxy 取代了 Object.defineProperty,成爲雙向綁定的底層原理,這個時候 Proxy 就顯得尤其重要。前端

本篇文章先以 Object.defineProperty 做爲引入,以後講解 Proxy,最後比較二者之間的優劣。git

一、Object.defineProperty 數據劫持

Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回此對象。es6

該方法接受三個參數,第一個參數是 obj:要定義屬性的對象,第二個參數是 prop:要定義或修改的屬性的名稱或 Symbol,第三個參數是 descriptor:要定義或修改的屬性描述符。github

const obj = {};
Object.defineProperty(obj, "property", {
  value18,
});
console.log(obj.property); // 18
複製代碼

雖然咱們能夠直接添加屬性和值,可是使用這種方式,咱們能進行更多的配置。面試

函數的第三個參數 descriptor 所表示的屬性描述符有兩種形式:數據描述符和存取描述符。數據描述符是一個具備值的屬性,該值能夠是可寫的,也能夠是不可寫的。存取描述符是由 getter 函數和 setter 函數所描述的屬性。一個描述符只能是這二者其中之一;不能同時是二者。編程

這兩種同時擁有下列兩種鍵值:數組

  1. configurable:當且僅當該屬性的 configurable 鍵值爲 true 時,該屬性的描述符纔可以被改變,同時該屬性也能從對應的對象上被刪除。默認爲 false。
  2. enumerable:當且僅當該屬性的 enumerable 鍵值爲 true 時,該屬性纔會出如今對象的枚舉屬性中。默認爲 false。
const obj = { property24 };
Object.defineProperty(obj, "property", {
  configurabletrue,
});
delete obj["property"]; // true
obj; // {}
// 改變狀態
const obj = { property24 };
Object.defineProperty(obj, "property", {
  configurablefalse,
});
delete obj["property"]; // false
obj; // {'property': 24}
複製代碼
const obj = {
  property124,
  property234,
  property354,
};
Object.defineProperty(obj, "property1", {
  enumerabletrue,
});
for (i in obj) {
  console.log(i);
}
// property1
// property2
// property3
// 改狀態
Object.defineProperty(obj, "property1", {
  enumerablefalse,
});
for (i in obj) {
  console.log(i);
}
// property2
// property3
複製代碼

數據描述符還具備如下可選鍵值:markdown

  1. value:該屬性對應的值。能夠是任何有效的 JavaScript 值(數值,對象,函數等)。默認爲 undefined。
  2. writable:當且僅當該屬性的 writable 鍵值爲 true 時,屬性的值,也就是上面的 value,才能被賦值運算符改變。默認爲 false。
const obj = {};
Object.defineProperty(obj, "property1", {
  value18,
});
obj; // {'property1': 18}
複製代碼
const obj = {};
Object.defineProperty(obj, "property1", {
  value18,
  writablefalse,
});
obj.property1 = 24;
obj; // {'property1': 18}
// 改變狀態
const obj = {};
Object.defineProperty(obj, "property1", {
  value18,
  writabletrue,
});
obj.property1 = 24;
obj; // {'property1': 24}
複製代碼

存取描述符還具備如下可選鍵值:app

  1. get:屬性的 getter 函數,若是沒有 getter,則爲 undefined。當訪問該屬性時,會調用此函數。執行時不傳入任何參數,可是會傳入 this 對象(因爲繼承關係,這裏的 this 並不必定是定義該屬性的對象)。該函數的返回值會被用做屬性的值。默認爲 undefined。
  2. set:屬性的 setter 函數,若是沒有 setter,則爲 undefined。當屬性值被修改時,會調用此函數。該方法接受一個參數(也就是被賦予的新值),會傳入賦值時的 this 對象。默認爲 undefined。
const obj = {};
Object.defineProperty(obj, "property1", {
  get(value) {
    return value;
  },
  set(newValue) {
    value = newValue;
  },
});
複製代碼

二、Proxy 數據攔截

Object.defineProperty 只能重定義獲取和設置的行爲,而 Proxy 至關於一個升級,它重定義了更多的行爲,接下來咱們對其進行深刻講解。函數

首先,Proxy 是一個構造函數,能夠經過 new 來建立它的實例,其接受兩個參數,一個是 target:要使用 Proxy 包裝的目標對象(能夠是任何類型的對象,包括原生數組,函數,甚至另外一個代理)。另一個是 handler:一個一般以函數做爲屬性的對象,各屬性中的函數分別定義了在執行各類操做時代理實例的行爲。

let p = new Proxy(target, handler);
複製代碼

handler 對象的方法

handler 中的全部方法都是可選的,若是沒有定義哪一個方法,那就保留原對象的默認行爲。

一、get()

用於攔截對象的讀取操做。該方法接收三個參數,target:目標對象,property:被獲取的屬性名,receiver:Proxy 或者繼承 Proxy 的對象。

let person = {
  name"Jack",
};
let p = new Proxy(person, {
  get(target, property) {
    if (property in target) {
      return target[property];
    } else {
      throw Error("不存在該屬性");
    }
  },
});
p.name; // Jack
p.age; // Uncaught Error: 不存在該屬性
複製代碼

查看第三個屬性

let person = {
  name"Jack",
};
let p = new Proxy(person, {
  get(target, property, receiver) {
    return receiver;
  },
});
p.name === p; // true
複製代碼

上面代碼中,p 對象的 name 屬性是由 p 對象提供的,因此 receiver 指向 proxy 對象。

二、set()

用於設置屬性值操做的捕獲器。該方法接收四個參數,target:目標對象,property:將被設置的屬性名或 Symbol,value:新屬性值,receiver:最初被調用的對象。一般是 proxy 自己,但 handler 的 set 方法也有可能在原型鏈上,或以其餘方式被間接地調用(所以不必定是 proxy 自己)。

let person = {};
let p = new Proxy(person, {
  set(target, property, value, receiver) {
    target[property] = value;
  },
});
p.name = 2;
複製代碼

當給 Proxy 的實例添加屬性的時候,就會調用 set()方法。

下面咱們來看一下 set()方法的第四個參數,通常狀況下都是指向 proxy 自己。

let person = {};
let p = new Proxy(person, {
  set(target, property, value, receiver) {
    target[property] = receiver;
  },
});
p.name = 2;
p.name === p; // true
複製代碼

可是也有其餘狀況,咱們借用阮一峯:ECMAScript 6 入門-Proxy中談到的例子:

const handler = {
  setfunction (obj, prop, value, receiver{
    obj[prop] = receiver;
  },
};
const proxy = new Proxy({}, handler);
const myObj = {};
Object.setPrototypeOf(myObj, proxy);

myObj.foo = "bar";
myObj.foo === myObj; // true
複製代碼

上面代碼中,設置 myObj.foo 屬性的值時,myObj 並無 foo 屬性,所以引擎會到 myObj 的原型鏈去找 foo 屬性。myObj 的原型對象 proxy 是一個 Proxy 實例,設置它的 foo 屬性會觸發 set 方法。這時,第四個參數 receiver 就指向原始賦值行爲所在的對象 myObj。

值得注意一點的是,當該對象不可配置不可編寫的時候,那麼 set()方法將不起做用。

const obj = {};
Object.defineProperty(obj, "foo", {
  value18,
  writablefalse,
});
let p = new Proxy(obj, {
  set(target, property, value, receiver) {
    target[property] = value;
  },
});
p.foo = 28;
p.foo; // 18
複製代碼

三、apply()

用於攔截函數的調用。該方法接受三個參數,target:目標對象(函數)。thisArg:被調用時的上下文對象。argumentsList:被調用時的參數數組。

當 Prxoy 的實例當作函數使用時,就會執行該方法。

let p = new Proxy(function ({}, {
  apply(target, thisArg, argumentsList) {
    console.log("Hello Word");
  },
});
p(); // Hello Word
複製代碼

四、has()

是針對 in 操做符的代理方法。該方法接受兩個參數,target:目標對象。prop:須要檢查是否存在的屬性。

let p = new Proxy(
  {},
  {
    has(target, prop) {
      console.log(target, prop); // {} 'a'
    },
  }
);
console.log("a" in p); // false
複製代碼

has()方法只對 in 運算符有效,對 for...in...運算沒有實際做用。

let p = new Proxy(
  { value18 },
  {
    has(target, prop) {
      if (prop === "value" && target[prop] < 20) {
        console.log("數值小於20");
        return false;
      }
      return prop in target;
    },
  }
);
"value" in p; // 數值小於20 false
for (let a in p) {
  console.log(p[a]); // 18
}
複製代碼

從上述例子能夠看出,has 方法攔截只對 in 運算符有效,對 for...in...來講沒有攔截效果。

五、construct

用於攔截 new 操做符. 爲了使 new 操做符在生成的 Proxy 對象上生效,用於初始化代理的目標對象自身必須具備[[Construct]]內部方法(即 new target 必須是有效的)。該方法接收三個參數,target:目標對象。argumentsList:constructor 的參數列表。newTarget:最初被調用的構造函數。

let p = new Proxy(function ({}, {
  construct(target, argumentsList, newTarget) {
    return { value: argumentsList[0] };
  },
})(new p(1)).value; // 1
複製代碼

construct()方法必須返回一個對象,否則不報錯。

let p = new Proxy(function ({}, {
  construct(target, argumentsList, newTarget) {
    console.log("Hello Word");
  },
});
new p(); // Uncaught TypeError: 'construct' on proxy: trap returned non-object ('undefined')
複製代碼

六、deleteProperty

用於攔截對對象屬性的 delete 操做。該方法接受兩個參數。target:目標對象。property:待刪除的屬性名。

let p = new Proxy(
  {},
  {
    deleteProperty(target, property) {
      console.log("called: " + property);
      return true;
    },
  }
);
delete p.a; // "called: a"
複製代碼

若是這個方法拋出錯誤或者返回 false,當前屬性就沒法被 delete 命令刪除。注意,目標對象自身的不可配置(configurable)的屬性,不能被 deleteProperty 方法刪除,不然報錯。

七、defineProperty()

用於攔截對對象的 Object.defineProperty() 操做。該方法接受三個參數,target:目標對象。property:待檢索其描述的屬性名。descriptor:待定義或修改的屬性的描述符。

let p = new Proxy(
  {},
  {
    definePropertyfunction (target, prop, descriptor{
      console.log("called: " + prop);
      return true;
    },
  }
);

let desc = { configurabletrueenumerabletruevalue10 };
Object.defineProperty(p, "a", desc); // "called: a"
複製代碼

若是 defineProperty()方法內部沒有任何操做,只返回 false,致使添加新屬性老是無效。注意,這裏的 false 只是用來提示操做失敗,自己並不能阻止添加新屬性。

注意,若是目標對象不可擴展(non-extensible),則 defineProperty()不能增長目標對象上不存在的屬性,不然會報錯。另外,若是目標對象的某個屬性不可寫(writable)或不可配置(configurable),則 defineProperty()方法不得改變這兩個設置。

八、defineProperty()

用法是攔截 Object.getOwnPropertyDescriptor(),必須返回一個 object 或 undefined。該方法接受兩個參數,target:目標對象。prop:返回屬性名稱的描述。

let p = new Proxy(
  { a20 },
  {
    getOwnPropertyDescriptorfunction (target, prop{
      console.log("called: " + prop);
      return { configurabletrueenumerabletruevalue10 };
    },
  }
);

console.log(Object.getOwnPropertyDescriptor(p, "a").value); // "called: a"
// 10
複製代碼

九、getPrototypeOf()

是一個代理(Proxy)方法,當讀取代理對象的原型時,該方法就會被調用。該方法只接受一個參數,target:被代理的目標對象。返回值必須是一個對象或者 null。 觸發該方法的條件總共有 5 種:

  1. Object.getPrototypeOf()
  2. Reflect.getPrototypeOf()
  3. __proto__
  4. Object.prototype.isPrototypeOf()
  5. instanceof
let proto = {};
let p = new Proxy(
  {},
  {
    getPrototypeOf(target) {
      return proto;
    },
  }
);
Object.getPrototypeOf(p) === proto; // true
複製代碼

十、isExtensible()

用於攔截對對象的 Object.isExtensible()。該方法接受一個參數。target:目標對象。返回值必須返回一個 Boolean 值或可轉換成 Boolean 的值。

let p = new Proxy(
  {},
  {
    isExtensiblefunction (target{
      console.log("called");
      return true;
    },
  }
);

console.log(Object.isExtensible(p)); // "called"
// true
複製代碼

注意,該方法有一個強制的約束,即 Object.isExtensible(proxy) 必須同 Object.isExtensible(target)返回相同值。也就是必須返回 true 或者爲 true 的值,返回 false 和爲 false 的值都會報錯。

let p = new Proxy(
  {},
  {
    isExtensiblefunction (target{
      return false;
    },
  }
);

Object.isExtensible(p); // Uncaught TypeError: 'isExtensible' on proxy: trap result does not reflect extensibility of proxy target (which is 'true')
複製代碼

十一、ownKeys()

用來攔截對象自身屬性的讀取操做。該方法接受一個參數,target:目標對象。

具體攔截以下:

  1. Object.getOwnPropertyNames()
  2. Object.getOwnPropertySymbols()
  3. Object.keys()
  4. for...in 循環

該方法有幾個約束條件:

  1. ownKeys 的結果必須是一個數組
  2. 數組的元素類型要麼是一個 String ,要麼是一個 Symbol
  3. 結果列表必須包含目標對象的全部不可配置(non-configurable )、自有(own)屬性的 key
  4. 若是目標對象不可擴展,那麼結果列表必須包含目標對象的全部自有(own)屬性的 key,不能有其它值
let target = {
  a1,
  b2,
  c3,
};
let p = new Proxy(target, {
  ownKeys(target) {
    return ["a"];
  },
});
Object.keys(p); // ['a']
複製代碼

十二、preventExtensions()

用於設置對 Object.preventExtensions()的攔截。該方法接受一個參數,target:所要攔截的目標對象。該方法返回一個布爾值。該方法有個限制,即若是目標對象是可擴展的,那麼只能返回 false。

let p = new Proxy(
  {},
  {
    preventExtensionsfunction (target{
      return true;
    },
  }
);

Object.preventExtensions(p); // Uncaught TypeError: 'preventExtensions' on proxy: trap returned truish but the proxy target is extensible
複製代碼

上面代碼中,proxy.preventExtensions()方法返回 true,但這時 Object.isExtensible(proxy)會返回 true,所以報錯。

爲了防止出現這個問題,一般要在 proxy.preventExtensions()方法裏面,調用一次 Object.preventExtensions()。

let p = new Proxy(
  {},
  {
    preventExtensionsfunction (target{
      console.log("called");
      Object.preventExtensions(target);
      return true;
    },
  }
);

console.log(Object.preventExtensions(p)); // "called"
// false
複製代碼

1三、setPrototypeOf()

用來攔截 Object.setPrototypeOf()。該方法接受兩個參數,target:被攔截目標對象。prototype:對象新原型或爲 null。若是成功修改了[[Prototype]], setPrototypeOf 方法返回 true,不然返回 false。

var handler = {
  setPrototypeOf(target, proto) {
    throw new Error("Changing the prototype is forbidden");
  },
};
var proto = {};
var target = function ({};
var proxy = new Proxy(target, handler);
Object.setPrototypeOf(proxy, proto);
// Error: Changing the prototype is forbidden
複製代碼

上面代碼中,只要修改 target 的原型對象,就會報錯。

注意,若是目標對象不可擴展(non-extensible),setPrototypeOf()方法不得改變目標對象的原型。

三、Object.defineProperty 和 Proxy 的區別

最主要的區別實際上是應用到 Vue 的雙向數據綁定中,其實也有因爲 Vue 的雙向數據綁定,才讓這兩個方法愈來愈多的進入到人們的視野中。

在此咱們不深刻講解雙向數據綁定,因此分別用這兩個方法實現個簡單版的雙向數據綁定,來看看二者之間的區別。

3.一、Object.defineProperty 版

const obj = {};
Object.defineProperty(obj, "text", {
  getfunction ({
    console.log("get val");
  },
  setfunction (newVal{
    console.log("set val:" + newVal);
    document.getElementById("input").value = newVal;
    document.getElementById("span").innerHTML = newVal;
  },
});

const input = document.getElementById("input");
input.addEventListener("keyup"function (e{
  obj.text = e.target.value;
});
複製代碼

能夠看出來這個簡單版的透露出Object.defineProperty一個很明顯的缺點,就是隻能對對象的一個屬性進行監聽,若是想要對對象的全部屬性監聽的話,就要去遍歷,而且還有一個問題就是沒法去監聽數組,可是仍是有優勢的,優勢就是兼容性好,這也是爲何Vue2.0優先選擇了Object.defineProperty的緣由。

3.二、Proxy 版

const input = document.getElementById('input');
const p = document.getElementById('p');
const obj = {};

const newObj = new Proxy(obj, {
  getfunction(target, key, receiver{
    console.log(`getting ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  setfunction(target, key, value, receiver{
    console.log(target, key, value, receiver);
    if (key === 'text') {
      input.value = value;
      p.innerHTML = value;
    }
    return Reflect.set(target, key, value, receiver);
  },
});

input.addEventListener('keyup'function(e{
  newObj.text = e.target.value;
});
複製代碼

從上述能夠看出Proxy能夠對整個對象進行攔截,而且其能返回一個新的對象,除此以外其能對數組進行攔截。並且最直觀的就是其有13個攔截方式。可是其最致命的問題就是兼容性很差,並且沒法用polyfill磨平,所以尤大大才聲明須要等到下個大版本(3.0)才能用Proxy重寫。

後語

相關文章:

以爲還能夠的,麻煩走的時候能給點個贊,你們一塊兒學習和探討!

還能夠關注個人博客但願能給個人github上點個Start,小夥伴們必定會發現一個問題,個人全部用戶名幾乎都與番茄有關,由於我真的很喜歡吃番茄❤️!!!

想跟車不迷路的小夥還但願能夠關注公衆號 前端老番茄 或者掃一掃下面的二維碼👇👇👇。

我是一個編程界的小學生,您的鼓勵是我不斷前進的動力,😄但願能一塊兒加油前進。

本文使用 mdnice 排版

相關文章
相關標籤/搜索