JavaScript 的 setter、getter 和 pro

2019-02-20  本文已影响26人  VioletJack

今天来学习下 JavaScript 的对象中的 setter、getter 和 proxy。

对象属性值的 [[Get]] 和 [[Put]] 操作

我们对于对象属性值的常用操作无非就是创建、修改和读取(删除操作想必都用的不多)。而对象属性值获取其实是对象属性值的 [[Get]] 操作,对象属性值的创建和修改是对象属性值的 [[Put]] 操作。

[[Get]]

获取对象属性值就是一次属性访问,访问具体步骤为:

  1. 在语言规范中,对象实际上是实现了 [[Get]] 操作的。对象默认内置的 [[Get]] 操作会在对象中查找是否有名称相同的属性,并返回这个属性的值。(这里其实还要注意 getter)
  2. 如果当前对象中并没有找到属性,就根据原型链向下查找。如果在原型链中找到同名属性则返回属性的值(找原型对象属性的方式还是用 [[Get]] 操作),如果找到原型链底端都没有找到则返回 undefined。

[[Put]]

与 [[Get]] 操作相对,[[Put]] 操作一般用于设置或创建对象的属性。但是 [[Put]] 操作要比 [[Get]] 操作更加麻烦一些:

  1. 检查对象属性是否设置了 setter,如果是就调用 setter。
  2. 检查对象属性的属性描述符中的 writable 是否为 false,如果是则无法进行修改。
  3. 检查对象中是否存在这个属性,如果存在则设置属性的值。
  4. 遍历原型链,检查对象的原型链对象中是否有这个属性。如果原型链中没有,则在对象上创建这个属性。
  5. 如果对象的原型链对象中有这个属性,就比较麻烦了。它分为三种情况:
    1. 如果在对象的原型链对象上存在同名属性,且属性标识符是可写的(writable:true),那就会在对象上创建这个属性,如此对象上的属性将屏蔽原型链对象上属性,称为屏蔽属性
    2. 如果在对象的原型链对象上存在同名属性,且属性标识符是只读的(writable:false),那么就无法继续赋值,在严格模式下还会报错。
    3. 如果在对象的原型链对象上存在同名属性,且属性是一个 setter,那么只会调用这个 setter。

setter 和 getter

上面提到了一些 setter 和 getter 的知识,那它们到底是什么呢?

在 ES5 中,可以使用 getter 和 setter 改写对象属性的默认操作。getter 会在获取属性值时调用,setter 会在设置属性值时调用。getter 和 setter 都是隐藏函数。

下面是 setter 和 getter 的两种定义方式:

// 方式 1
var obj = {
  get a() {
    return this.__a__ + 100
  },
  set a(val) {
    this.__a__ = val
  }
};
// 方式 2
Object.defineProperty(obj, "b", {
  get: function() {
    return this.__b__ * -1
  },
  set: function (val) {
    this.__b__= val / 2
  }
});
// setters
obj.a = 12
obj.b = 20
// getters
console.log(obj.a) // 112
console.log(obj.b) // -10

console.log(obj) // { __a__: 12, __b__: 10 }

可以看到我们传入的值分别为 12 和 20,但是在保存到对象变量前由于 setter 的计算,最后保存的值为 12 和 10。而在读取 a 和 b 时,获取到了 getter 的计算结果。

注意:当对象属性使用访问描述符(setter & getter)后,JavaScript 将忽略 value 和 writable 属性描述符。

var obj = {
  b: 123
} // { b: 123 }

Object.defineProperty(obj, "b", {
  get: function() {
    return this.__b__ * -1
  },
  set: function (val) {
    this.__b__= val / 2
  }
});

obj // {}

Object.getOwnPropertyDescriptor(obj, "b")

// {
//   onfigurable: true,
//   enumerable: true
//   get: f (),
//   set: f (val)
// }

Proxy

Proxy 用于封装一个普通对象,并返回一个新对象。它接收两个参数,第一个参数是被代理的普通对象,第二个参数是代理行为对象。

var obj1 = {
  a: 1
} // { a: 1 }

var obj2 = new Proxy(obj1, {
  get: function (target, key, receiver) {
    console.log(`getting ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, value, receiver) {
    console.log(`setting ${key}!`);
    return Reflect.set(target, key, value, receiver);
  }
}) // Proxy { a: 1 }

obj2.a
// getting a!
obj2.a++
// getting a!
// setting a!
obj1 // { a: 2 }

上面例子中使用 Proxy 代理了 obj1 对象的 setter 和 getter 行为,当 obj1 有 setter 或 getter 行为时都会先经过 proxy 中的 getter 和 setter 方法。本例中的代理方法使用 Reflect.set(...) 和 Reflect.get(...) 来设置和获取对象中的属性,所以 obj1 的属性随之改变。

下面是 Proxy 中实例方法的整理

apply方法的使用

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

var p = new Proxy(target, handler);

console.log(p())
// "I am the proxy"

has方法的使用

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

construct方法的使用

var p = new Proxy(function () {}, {
    construct: function(target, args) {
        console.log('called: ' + args.join(', '));
        return { value: args[0] * 5 + 12 };
    }
});

console.log(new p(1))
console.log(new p(1).value)

// call: 1
// { value: 17 }
// call: 1
// 17

代理在某些处理对象属性的场景下是非常好用的,这个之后我们可以继续探讨下~

参考资料

上一篇下一篇

猜你喜欢

热点阅读