首頁 >web前端 >js教程 >ES6中defineProperty與proxy的詳細介紹(程式碼範例)

ES6中defineProperty與proxy的詳細介紹(程式碼範例)

不言
不言轉載
2018-11-15 16:59:312410瀏覽

本篇文章帶給大家的內容是關於ES6中defineProperty與proxy的詳細介紹(程式碼範例),有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

我們或多或少都聽過「資料綁定」這個詞,「資料綁定」的關鍵在於監聽資料的變化,可是對於這樣一個物件:var obj = {value: 1},我們該怎麼知道obj 發生了改變呢?

definePropety

ES5 提供了 Object.defineProperty 方法,該方法可以在一個物件上定義一個新屬性,或修改一個物件的現有屬性,並傳回這個物件。

語法

Object.defineProperty(obj, prop, descriptor)

參數

obj: 要在其上定义属性的对象。
prop:  要定义或修改的属性的名称。
descriptor: 将被定义或修改的属性的描述符。

舉例:

var obj = {};
Object.defineProperty(obj, "num", {
    value : 1,
    writable : true,
    enumerable : true,
    configurable : true
});
//  对象 obj 拥有属性 num,值为 1

雖然我們可以直接加入屬性和值,但是使用這種方式,我們可以進行更多的配置。

函數的第三個參數 descriptor 所表示的屬性描述符有兩種形式:資料描述子和存取描述子

兩者皆有下列兩種鍵值

configurable

當且僅當該屬性的 configurable 為true 時,此屬性描述子才能夠被改變,也能夠被刪除。預設為 false。

enumerable

當且僅當此屬性的 enumerable 為 true 時,此屬性才能夠出現在物件的列舉屬性中。預設為 false。

資料描述子同時具有以下可選鍵值

#value

此屬性對應的值。可以是任何有效的 JavaScript 值(數值,對象,函數等)。預設為 undefined。

writable

當且僅當該屬性的 writable 為 true 時,此屬性才能被賦值運算子改變。預設為 false。

訪問描述子同時具有以下可選鍵值

#get

一個給屬性提供getter 的方法,如果沒有 getter 則為 undefined。此方法傳回值被用作屬性值。預設為 undefined。

set

一個提供屬性 setter 的方法,如果沒有 setter 則為 undefined。此方法將接受唯一參數,並將該參數的新值指派給該屬性。預設為 undefined。

值得注意的是:

屬性描述符必須是資料描述符或存取描述符兩種形式之一,不能同時是兩者。 這意味著你可以:

Object.defineProperty({}, "num", {
    value: 1,
    writable: true,
    enumerable: true,
    configurable: true
});

也可以:

var value = 1;
Object.defineProperty({}, "num", {
    get : function(){
      return value;
    },
    set : function(newValue){
      value = newValue;
    },
    enumerable : true,
    configurable : true
});

但是不可以:

// 报错
Object.defineProperty({}, "num", {
    value: 1,
    get: function() {
        return 1;
    }
});

此外,所有的屬性描述子都是非必須的,但是descriptor 這個欄位是必須的,如果不進行任何配置,你可以這樣:

var obj = Object.defineProperty({}, "num", {});
console.log(obj.num); // undefined

Setters 和Getters

之所以講到defineProperty,是因為我們要使用存取描述符中的get 和set,這兩個方法又被稱為getter 和setter。由 getter 和 setter 定義的屬性稱做」存取器屬性「。

當程式查詢存取器屬性的值時,JavaScript 呼叫 getter方法。這個方法的回傳值就是屬性存取表達式的值。當程式設定一個存取器屬性的值時,JavaScript 呼叫 setter 方法,將賦值表達式右邊的值當作參數傳入 setter。從某種意義上講,這個方法負責「設定」屬性值。可以忽略 setter 方法的回傳值。

舉個例子:

var obj = {}, value = null;
Object.defineProperty(obj, "num", {
    get: function(){
        console.log('执行了 get 操作')
        return value;
    },
    set: function(newValue) {
        console.log('执行了 set 操作')
        value = newValue;
    }
})

obj.value = 1 // 执行了 set 操作

console.log(obj.value); // 执行了 get 操作 // 1

這不就是我們要的監控資料改變的方法嗎?我們再來封裝一下:

function Archiver() {
    var value = null;
    // archive n. 档案
    var archive = [];

    Object.defineProperty(this, 'num', {
        get: function() {
            console.log('执行了 get 操作')
            return value;
        },
        set: function(value) {
            console.log('执行了 set 操作')
            value = value;
            archive.push({ val: value });
        }
    });

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

var arc = new Archiver();
arc.num; // 执行了 get 操作
arc.num = 11; // 执行了 set 操作
arc.num = 13; // 执行了 set 操作
console.log(arc.getArchive()); // [{ val: 11 }, { val: 13 }]

watch API

既然可以監控資料的改變,那我可以這樣設想,也就是當資料改變的時候,自動進行渲染工作。舉個例子:

HTML 中有一個 span 標籤和 button 標籤

e729387c80c75e8349ab06d70943ab81154bdf357c58b8a65c66d7c19c8e4d114
be83eb51f340b921e483a6b46a36fe5f点击加 165281c5ac262bf6d81768915a4a77ac0

當點擊按鈕的時候,span 標籤裡的值加 1。

傳統的做法是:

document.getElementById('button').addEventListener("click", function(){
    var container = document.getElementById("container");
    container.innerHTML = Number(container.innerHTML) + 1;
});

如果使用了defineProperty:

var obj = {
    value: 1
}

// 储存 obj.value 的值
var value = 1;

Object.defineProperty(obj, "value", {
    get: function() {
        return value;
    },
    set: function(newValue) {
        value = newValue;
        document.getElementById('container').innerHTML = newValue;
    }
});

document.getElementById('button').addEventListener("click", function() {
    obj.value += 1;
});

程式碼看似增多了,但是當我們需要改變span 標籤裡的值的時候,直接修改obj.value 的值就可以了。

然而,現在的寫法,我們還需要單獨宣告一個變數儲存obj.value 的值,因為如果你在set 中直接 obj.value = newValue 就會陷入無限的循環中。另外,我們可能需要監控很多屬性值的改變,要是一個一個寫,也很累吶,所以我們簡單寫個 watch 函數。使用效果如下:

var obj = {
    value: 1
}

watch(obj, "num", function(newvalue){
    document.getElementById('container').innerHTML = newvalue;
})

document.getElementById('button').addEventListener("click", function(){
    obj.value += 1
});

我們來寫下這個watch 函數:

(function(){
    var root = this;
    function watch(obj, name, func){
        var value = obj[name];

        Object.defineProperty(obj, name, {
            get: function() {
                return value;
            },
            set: function(newValue) {
                value = newValue;
                func(value)
            }
        });

        if (value) obj[name] = value
    }

    this.watch = watch;
})()

現在我們已經可以監控物件屬性值的改變,並且可以根據屬性值的改變,添加回呼函數,棒棒噠~

proxy

使用defineProperty 只能重定義屬性的讀取(get)和設定(set)行為,到了ES6,提供了Proxy,可以重定義更多的行為,例如in、delete、函數呼叫等更多行為。

Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。我们来看看它的语法:

var proxy = new Proxy(target, handler);

proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。

var proxy = new Proxy({}, {
    get: function(obj, prop) {
        console.log('设置 get 操作')
        return obj[prop];
    },
    set: function(obj, prop, value) {
        console.log('设置 set 操作')
        obj[prop] = value;
    }
});

proxy.time = 35; // 设置 set 操作

console.log(proxy.time); // 设置 get 操作 // 35

除了 get 和 set 之外,proxy 可以拦截多达 13 种操作,比如 has(target, propKey),可以拦截 propKey in proxy 的操作,返回一个布尔值。

// 使用 has 方法隐藏某些属性,不被 in 运算符发现
var handler = {
  has (target, key) {
    if (key[0] === '_') {
      return false;
    }
    return key in target;
  }
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);
console.log('_prop' in proxy); // false

又比如说 apply 方法拦截函数的调用、call 和 apply 操作。

apply 方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组,不过这里我们简单演示一下:

var target = function () { return 'I am the target'; };
var handler = {
  apply: function () {
    return 'I am the proxy';
  }
};

var p = new Proxy(target, handler);

p();
// "I am the proxy"

又比如说 ownKeys 方法可以拦截对象自身属性的读取操作。具体来说,拦截以下操作:

  • Object.getOwnPropertyNames()

  • Object.getOwnPropertySymbols()

  • Object.keys()

下面的例子是拦截第一个字符为下划线的属性名,不让它被 for of 遍历到。

let target = {
  _bar: 'foo',
  _prop: 'bar',
  prop: 'baz'
};

let handler = {
  ownKeys (target) {
    return Reflect.ownKeys(target).filter(key => key[0] !== '_');
  }
};

let proxy = new Proxy(target, handler);
for (let key of Object.keys(proxy)) {
  console.log(target[key]);
}
// "baz"

更多的拦截行为可以查看阮一峰老师的 《ECMAScript 6 入门》

值得注意的是,proxy 的最大问题在于浏览器支持度不够,而且很多效果无法使用 poilyfill 来弥补。

watch API 优化

我们使用 proxy 再来写一下 watch 函数。使用效果如下:

(function() {
    var root = this;

    function watch(target, func) {

        var proxy = new Proxy(target, {
            get: function(target, prop) {
                return target[prop];
            },
            set: function(target, prop, value) {
                target[prop] = value;
                func(prop, value);
            }
        });

        if(target[name]) proxy[name] = value;
        return proxy;
    }

    this.watch = watch;
})()

var obj = {
    value: 1
}

var newObj = watch(obj, function(key, newvalue) {
    if (key == 'value') document.getElementById('container').innerHTML = newvalue;
})

document.getElementById('button').addEventListener("click", function() {
    newObj.value += 1
});

我们也可以发现,使用 defineProperty 和 proxy 的区别,当使用 defineProperty,我们修改原来的 obj 对象就可以触发拦截,而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截。

以上是ES6中defineProperty與proxy的詳細介紹(程式碼範例)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除