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

会 Proxy 这么了不起吗 #14

Open
kangkai124 opened this issue Apr 12, 2019 · 5 comments
Open

会 Proxy 这么了不起吗 #14

kangkai124 opened this issue Apr 12, 2019 · 5 comments

Comments

@kangkai124
Copy link
Owner

proxy

抱歉,会 Proxy 真的可以为所欲为。

@kangkai124
Copy link
Owner Author

get(target, propKey, receiver)

get方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。

let proxy = new Proxy({}, {
  get (target, name) {
    console.log('getting ' + name)
    return target[name]
  }
})

proxy.hello	// getting hello

get 方法可以继承。

// 接上面代码
let obj = Object.create(proxy)
obj.foo		// getting foo

get方法的第三个参数,它总是指向原始的读操作所在的那个对象,一般情况下就是 Proxy 实例。

const proxy = new Proxy({}, {
  get: function(target, property, receiver) {
    return receiver;
  }
});
proxy.getReceiver === proxy // true

const d = Object.create(proxy);
d.a === d // true

上面代码中,proxy对象的getReceiver属性是由proxy对象提供的,所以receiver指向proxy对象。d对象本身没有a属性,所以读取d.a的时候,会去d的原型proxy对象找。这时,receiver就指向d,代表原始的读操作所在的那个对象。

如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,否则通过 Proxy 对象访问该属性会报错。

const target = Object.defineProperties({}, {
  foo: {
    value: 123,
    writable: false,
    configurable: false
  },
});

const handler = {
  get(target, propKey) {
    return 'abc';
  }
};

const proxy = new Proxy(target, handler);

proxy.foo
// TypeError: Invariant check failed

更多应用

1. 使用get拦截,实现数组读取负数的索引

function createArray (...elements) {
  let handler = {
    get (target, key, receiver) {
      let index = Number(key)
      if (index < 0) {
        key = String(target.length + index)
      }
      return Reflect.get(target, key, receiver)
    }
  }
  
  let target = []
  target.push(...elements)
  return new Proxy(target, handler)
}

let arr = createArray(1, 2, 3)
arr[-1]		// 3

2. 链式操作

let pipe = (function () {
  return function (value) {
    let funcStack = []
    let oProxy = new Proxy({}, {
      get (pipObj, fnName) {
        if (fnName === 'get') {
          return funcStack.reduce((val, fn) => fn(val), value)
        }
        funcStack.push(window[fnName])
        return oProxy
      }
    })
    return oProxy
  }
}())

let double = n => n * 2
let pow = n => n * n
let reverseInt = n => n.toString().split('').reverse().join('') || 0

pipe(3).double.pow.reverseInt.get		// 63

3. 生成各种 DOM 节点的通用函数dom

let dom = new Proxy({}, {
  get (target, property) {
    return function (attrs = {}, ...children) {
      const el = document.createElement(property)
      for (let prop of Object.keys(attrs)) {
        el.setAttribute(prop, attrs[prop])
      }
      for (let child of children) {
        if (typeof child === 'string') {
          child = document.createTextNode(child)
        }
        el.appendChild(child)
      }
      return el
    }
  }
})

const el = dom.div({},
  'Hello, my name is ',
  dom.a({href: '//example.com'}, 'Mark'),
  '. I like:',
  dom.ul({},
    dom.li({}, 'The web'),
    dom.li({}, 'Food'),
    dom.li({}, '…actually that\'s it')
  )
)

document.body.appendChild(el)

@kangkai124
Copy link
Owner Author

Proxy实现一个简单的双向绑定的 todo list

<div id="app">
    <input type="text" id="input">
    <div>
      TODO:
      <span id="text"></span>
    </div>
    <div id="btn">Add To Todo List</div>
    <ul id="list"></ul>
  </div>
const input = document.getElementById('input')
    const text = document.getElementById('text')
    const list = document.getElementById('list')
    const btn = document.getElementById('btn')

    let render

    const inputObj = new Proxy({}, {
      get (target, key, receiver) {
        return Reflect.get(target, key, receiver)
      },
      set (target, key, value, receiver) {
        if (key === 'text') {
          input.value = value
          text.innerHTML = value
        }
        return Reflect.set(target, key, value, receiver)
      }
    })

    class Render {
      constructor (arr) {
        this.arr = arr
      }
      init () {
        const fragment = document.createDocumentFragment()
        for (let i = 0; i < this.arr.length; i++) {
          const li = document.createElement('li')
          li.textContent = this.arr[i]
          fragment.appendChild(li)
        }
        list.appendChild(fragment)
      }
      addList (val) {
        const li = document.createElement('li')
        li.textContent = val
        list.appendChild(li)
      }
    }

    const todoList = new Proxy([], {
      get (target, key, receiver) {
        return Reflect.get(target, key, receiver)
      },
      set (target, key, value, receiver) {
        if (key !== 'length') {
          render.addList(value)
        }
        return Reflect.set(target, key, value, receiver)
      }
    })

    window.onload = () => {
      render = new Render([])
      render.init()
    }

    input.addEventListener('keyup', e => {
      inputObj.text = e.target.value
    })

    btn.addEventListener('click', () => {
      todoList.push(inputObj.text)
      inputObj.text = ''
    })

@kangkai124
Copy link
Owner Author

set(target, key, value, receiver)

set 方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。

禁止读写内部属性

const handler = {
  get (target, key) {
    invariant(key, 'get');
    return target[key];
  },
  set (target, key, value) {
    invariant(key, 'set');
    target[key] = value;
    return true;
  }
};
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Invalid attempt to ${action} private "${key}" property`);
  }
}
const target = {};
const proxy = new Proxy(target, handler);
proxy._prop
// Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c'
// Error: Invalid attempt to set private "_prop" property

set 方法的第四个参数 receiver,指的是原始的操作行为所在的那个对象,一般情况下是proxy实例本身。

const handler = {
  set: function(obj, prop, value, receiver) {
    obj[prop] = receiver;
  }
};
const proxy = new Proxy({}, handler);
const myObj = {};
// myObj 原型指向 proxy
Object.setPrototypeOf(myObj, proxy);

myObj.foo = 'bar';
myObj.foo === myObj // true

上面代码中,设置 myObj.foo 属性的值时,myObj 并没有 foo 属性,因此引擎会到 myObj 的原型链去找 foo 属性。myObj 的原型对象 proxy 是一个 Proxy 实例,设置它的 foo 属性会触发set 方法(不太懂,为什么会设置原型对象的 foo 属性???)。这时,第四个参数 receive r就指向原始赋值行为所在的对象 myObj

注意,如果目标对象自身的某个属性,不可写且不可配置,那么set方法将不起作用。

严格模式下,set代理如果没有返回true,就会报错。

@kangkai124
Copy link
Owner Author

apply(target, ctx, args)

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"


var twice = {
  apply (target, ctx, args) {
    return Reflect.apply(...arguments) * 2;
  }
};
function sum (left, right) {
  return left + right;
};
var proxy = new Proxy(sum, twice);
proxy(1, 2) // 6
proxy.call(null, 5, 6) // 22
proxy.apply(null, [7, 8]) // 30

@kangkai124
Copy link
Owner Author

Proxy相比Object.defineProperty的优势

  1. Proxy可以直接监听对象,而不只是属性
  2. Proxy可以直接监听数组的变化
  3. Proxy是标准,且还有其他10多种的拦截方法

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant