ES6 系列之 defineProperty 与 proxy

前言

咱们或多或少都听过“数据绑定”这个词,“数据绑定”的关键在于监听数据的变化,但是对于这样一个对象:var obj = {value: 1},咱们该怎么知道 obj 发生了改变呢?git

definePropety

ES5 提供了 Object.defineProperty 方法,该方法能够在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。es6

语法github

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

虽然咱们能够直接添加属性和值,可是使用这种方式,咱们能进行更多的配置。app

函数的第三个参数 descriptor 所表示的属性描述符有两种形式:数据描述符和存取描述符异步

二者均具备如下两种键值函数

configurable优化

当且仅当该属性的 configurable 为 true 时,该属性描述符才可以被改变,也可以被删除。默认为 false。

enumerablethis

当且仅当该属性的 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 标签

<span id="container">1</span>
<button id="button">点击加 1</button>

当点击按钮的时候,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 系列

ES6 系列目录地址:https://github.com/mqyqingfeng/Blog

ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级做用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。

若是有错误或者不严谨的地方,请务必给予指正,十分感谢。若是喜欢或者有所启发,欢迎 star,对做者也是一种鼓励。



本文做者:冴羽

阅读原文

本文为云栖社区原创内容,未经容许不得转载。

相关文章
相关标签/搜索