Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

es6-代理Proxy #23

Open
wozien opened this issue Oct 25, 2020 · 0 comments
Open

es6-代理Proxy #23

wozien opened this issue Oct 25, 2020 · 0 comments
Labels

Comments

@wozien
Copy link
Owner

wozien commented Oct 25, 2020

我们知道在es5中 Object.definePropety() 方法可以设置对象属性的 gettersetter,从而拦截对象属性的存取行为。但是局限性挺大,比如无法拦截数组的存取和其他对象的行为比如(indelele 等)。于是,es6引入了代理( Proxy ),它是一种可以拦截并改变底层js操作的包装器。

基本用法

new Proxy(target, handler) 新建一个代理对象,target 表示代理的目标对象,handler 是一个拦截行为的配置对象。

let obj = {
  name: 'wozien'
};

let proxy = new Proxy(obj, {});

console.log(proxy.name);
proxy.name = 'aaa';
console.log(obj.name);

handler 是个空对象时,代理只是简单的转发目标的默认行为。es6为我们提供了13种拦截行为的配置,并用 Reflect 对象提供操作JS的默认行为。

比如用 get() 拦截代理对象的值的获取行为:

let obj = {
  name: 'wozien'
};

obj = new Proxy(obj, {
  get(target, key) {
    console.log('proxy get');
    return Reflect.get(target, key);
  }
});
console.log(obj.name);

// proxy get
// wozien

set() 设置代理对象的值设置行为:

let obj = {
  name: 'wozien'
};

obj = new Proxy(obj, {
  set(target, key, value) {
    console.log('proxy set');
    return Reflect.set(target, key, value);
  }
});

obj.name = 'marry';   // proxy set
console.log(obj.name);  // marry

使用 has() 设置代理对象 in 操作行为,比如隐藏对象的某些属性:

let obj = {
  name: 'wozien',
  _age: 12
};

let proxy = new Proxy(obj, {
  has(target, key) {
    if (key[0] === '_') {
      return false;
    }
    return Reflect.has(target, key);
  }
});

console.log('_age' in proxy); // false

使用 ownKeys() 设置拦截对象的 Object.keys()for...in 等操作的行为:

let obj = {
  name: 'wozien',
  _age: 12
};

let proxy = new Proxy(obj, {
  ownKeys(target) {
    return Reflect.ownKeys(target).filter(key => {
      return typeof key !== 'string' || key[0] !== '_';
    });
  }
});

for (let key in proxy) {
  console.log(key);
}

// name

其他更多的代理拦截行为设置参考Proxy-MDN

可撤销代理

利用 Proxy.revocale() 方法创建可以撤销的代理对象。该方法返回一个含有下面两个属性的对象:

  • proxy: 可撤销的代理对象
  • revoke: 可撤销代理的方法

调用 revoke() 方法后,代理对象不能进一步操作:

let obj = {
  name: 'wozien'
};

let { proxy, revoke } = Proxy.revocable(obj, {});

console.log(proxy.name);  // wozien

revoke();

console.log(proxy.name); // Cannot perform 'get' on a proxy that has been revoked

应用

监测数组

我们知道在es5中无法检测数组的存取行为,利用 Proxy 可以轻松完成这个任务:

let proxy = new Proxy([], {
  get(arr, key) {
    console.log('proxy get ' + key);
    return arr[key];
  },
  set(arr, key, val) {
    console.log('proxy set ' + key + ': ' + val);
    return (arr[key] = val);
  }
});

proxy[0] = 1;

console.log(proxy[0]);

// proxy set 0: 1
// proxy get 0
// 1

替代Object.defineProperty()

Object.defineProperty() 是监测对象的属性存取,如果要使整个对象可监测,就要循环设置:

function define(obj, key) {
  let value = obj[key];
  Object.defineProperty(obj, key, {
    get() {
      console.log('get obj prop ' + key);
      return value;
    },
    set(newVal) {
      console.log('set obj prop ' + key + ' to ' + newVal);
      value = newVal;
    }
  });
}

function observer(obj) {
  for (let key in obj) {
    define(obj, key);
  }
}

let obj = {
  name: 'wozien',
  age: 23
};

// 侦测obj数据
observer(obj);

obj.name;
obj.name = 'marry';
obj.age = 25;

// get obj prop name
// set obj prop name to marry
// set obj prop age to 25

利用 Proxyset()get() 设置拦截对象的存取行为:

function observe(obj) {
  return new Proxy(obj, {
    get(target, key) {
      console.log('get obj prop ' + key);
      return target[key];
    },
    set(target, key, newVal) {
      console.log('set obj prop ' + key + ' to ' + newVal);
      return (target[key] = newVal);
    }
  });
}

let obj = {
  name: 'wozien',
  age: 23
};

obj = observe(obj);

obj.name;
obj.name = 'marry';
obj.age = 25;

模拟数组行为

我们知道数组可以通过设置不存在的索引扩大数组长度,通过设置小于数组长度时可以删除元素。在es5我们无法用对象模拟这种行为,而利用 Proxy 可以实现:

function isArrayIndex(value) {
  let num = Number(value);
  return typeof num === 'number' && num >= 0;
}

function createArray(length = 0) {
  return new Proxy(
    { length },
    {
      set(target, key, value) {
        let curLength = target.length;

        if (isArrayIndex(key)) {
          let num = Number(key);
          if (num >= curLength) {
            target.length = num + 1;
          }
        } else if (key === 'length') {
          if (value < curLength) {
            for (let index = curLength - 1; index >= value; index--) {
              delete target[index];
            }
          }
        }

        target[key] = value;
      }
    }
  );
}

let arr = createArray(1);

arr[0] = 11;
arr[1] = 12;
arr[2] = 13;

console.log(arr.length); // 3
console.log(arr[1]);  // 12

arr.length = 1;
console.log(arr.length); // 1
console.log(arr[1]); // undefiend

监测类属性类型

可以在类构造函数中返回实例对象的代理,代理可以在 set() 监测对象属性值的类型:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
    return new Proxy(this, {
      set(target, key, value) {
        if (typeof value !== Person.propTypes[key]) {
          throw new TypeError('property type error');
        }
        return Reflect.set(target, key, value);
      }
    });
  }
}

Person.propTypes = {
  name: 'string',
  age: 'number'
};

let person = new Person('wozien', 12);

person.name = 12; // throw new TypeError('property type error');

参考

ES6 系列之 defineProperty 与 proxy

@wozien wozien added the es6 label Oct 25, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant