Proxy与Object.defineProperty

1,521 阅读6分钟

Proxy与Object.defineProperty的对比

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

Object.defineProperty是使用的数据劫持:直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。数据劫持最典型的应用 -----> 双向的数据绑定(一个常用的面试题),

  • Vue 2.x 利用 Object.defineProperty(),并且把内部解耦为 Observer, Dep, 并使用 Watcher 相连

  • Vue3.x 版本之后改用 Proxy 进行实现

  • Object.defineProperty

    • 只能监听对象(Object),不能监听数组的变化,无法触发push, pop, shift, unshift,splice, sort, reverse

    • 必须遍历对象的每个属性

    • 只能劫持当前对象属性,如果想深度劫持,必须深层遍历嵌套的对象

      "use strict"
      let obj = {};
      let value = 1
      Object.defineProperty(obj, 'listenA', {
        writable: true,  //可修改
        enumerable: true,  // 可枚举   for...in...   Object.keys()
        configurable: true,  // 可配置,可删除
        get: () => value,
        set: val => {
          console.log(`set obj.listenA .. ${val}`);
          value = val
        },
      });
      obj.listenA = 2 //set obj.listenA .. 2
      console.log(obj.listenA)  // 2
      复制代码
      
  • Proxy

    • 可以直接监听对象而非属性

    • 可以直接监听数组的变化

      // 代理整个对象 let proxyObj = new Proxy({}, { get: (target, key, receiver) => { console.log(getting ${key}!); return target[key]; }, set: (target, key, value, receiver) => { console.log(target, key, value, receiver); return target[key] = value; }, }); proxyObj.val = 1; // {} val 1 {} proxyObj.val; // getting val!

      //代理数组 let proxyArr = new Proxy([], { get: (target, key, receiver) => { console.log(getting ${key}!); return target[key]; }, set: (target, key, value, receiver) => { console.log(target, key, value, receiver); return (target[key] = value); }, }); proxyArr[0] = 1; // {} val 1 {} console.log(proxyArr[0]); // getting val! // 1 console.log(proxyArr); // [1]

Reflect

Reflect翻译过来是反射的意思,与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。有一下几个作用

  • Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在ObjectReflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。

  • 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false

    // 老写法 try { Object.defineProperty(target, property, attributes); // success } catch (e) { // failure }

    // 新写法 if (Reflect.defineProperty(target, property, attributes)) { // 成功返回true // success } else { // failure } 复制代码

  • Object操作都变成函数行为。某些Object操作是命令式,比如name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让它们变成了函数行为。

    // 老写法 'assign' in Object // true

    // 新写法 Reflect.has(Object, 'assign') // true 复制代码

  • Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。

    Proxy(target, { set: function(target, name, value, receiver) { var success = Reflect.set(target, name, value, receiver); if (success) { console.log('property ' + name + ' on ' + target + ' set to ' + value); } return success; } });

async function

ES2017 标准引入了 async 函数,使得异步操作变得更加方便,由于async函数返回的是Promise对象,可以作为await命令的参数。

async function timeout(ms) {
  await new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
}

asyncPrint('hello world', 50);

返回 Promise 对象

async function f() {
  return 'hello world';
}

f().then(v => console.log(v))
// "hello world"
复制代码

async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到

async function f() {
  throw new Error('出错了');
}

f().then(
  v => console.log('resolve', v),
  e => console.log('reject', e)
)
//reject Error: 出错了

Promise 对象的状态变化

async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

async function getTitle(url) {
  let response = await fetch(url);
  let html = await response.text();
  return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('http://localhost:8080/').then(console.log(123))

await 命令

正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

async function f() {
  // 等同于
  // return 123;
  return await 123;
}

f().then(v => console.log(v))
// 123
复制代码

任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。

async function f() {
  await Promise.reject('出错了');
  await Promise.resolve('hello world'); // 不会执行
}

如果希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await放在try...catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行。

async function f() {
  try {
    await Promise.reject('出错了');
  } catch(e) {
  }
  return await Promise.resolve('hello world');
}

f()
.then(v => console.log(v))
// hello world

async 函数的实现原理

Generator 函数和自动执行器,包装在一个函数里

async function fn(args) {
  // ...
}

// 等同于

function fn(args) {
  return spawn(function* () {
    // ...
  });
}

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      if(next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}

严格模式

ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";

严格模式主要有以下限制。

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用with语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  • eval不会在它的外层作用域引入变量
  • evalarguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局对象
  • 不能使用fn.callerfn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protected、staticinterface

export && import

export && import来进行模块的导出导入。

// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};

// 报错
function f() {}
export f;
// 正确
export function f() {};
// 正确
function f() {}
export {f};

import 'lodash';
import 'lodash';  //加载了两次lodash,但是只会执行一次。

export { foo, bar } from 'my_module';  //当前模块不能直接使用foo和bar
// 接口改名
export { foo as myFoo } from 'my_module';
// 整体输出
export * from 'my_module';
//默认接口
export { default } from 'foo';
export { default as es6 } from './someModule';