【ES6标准入门】JavaScript的Proxy的使用和详情,还有代理的概念问题

在这里插入图片描述

作者简介:一名大三的学生,致力学习前端开发技术
⭐️个人主页:夜宵饽饽的主页
❔ 系列专栏:JavaScript进阶指南
学习格言:成功不是终点,失败也并非末日,最重要的是继续前进的勇气

​​前言:

这里是关于代理对象的使用Proxy,有部分方法的具体讲解,简单的方法没有做补充,大家有需要的话,可以去MDN上面查看。 这是我自己的学习JavaScript的笔记,希望可以帮助到大家,欢迎大家的补充和纠正

文章目录

    • 第12章 Proxy
      • 12.1 概述
      • 12.2 Proxy实例的方法
        • 12.2.1 get()
        • 12.2.2 set() :
        • 12.2.3 apply()
        • 12.2.4 has()
        • 12.2.5 construct()
        • 12.2.7 defineProperty()
        • 12.2.9 getPrototypeOf()
        • 12.2.11 ownKeys()
      • 12.3 Proxy.revocable()
      • 12.4 this问题

第12章 Proxy

12.1 概述

Proxy用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程” 即对语言进行编程

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

var obj=new Proxy({},{
    get:function(target,key,receiver){
        console.log(`getter ${key}`);
        return Reflect.get(target,key,receiver)
    },
    set:function(target,key,value,receiver){
        console.log(`setting ${key}`);
        return Reflect.get(target,key,value,receiver)
    }
})

obj.count=1
++obj.count

上面的代码说明,Proxy实际上重载了点运算符,即用自己定义的覆盖了语言的原始定义

ES6提供了Proxy构造函数,用于生成Proxy实例

var proxy=new Proxy(target,handler)

参数的含义为:

  • 第一个参数target表示使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。即如果没有proxy介入,操作原来访问的就是这个对象
  • 第二参数handler也是一个对象,用来定制拦截行为的,对于每一个被代理的操作,需要提供一个对应的处理函数

例子如下:

var proxy=new Proxy({},{
	get:function(target,property){
		return 35
	}
})

proxy.time //35
proxy.name //35
proxy.title //35

上面的代码中,配置对象有一个get方法用来拦截对目标对象属性的访问请求。get的两个参数,一个为目标对象,一个为所要访问的属性,可以看到,由于拦截函数总数返回35,所以访问任意的属性都是返回35

❗️ 注意:要使Proxy起作用,必须针对Proxy实例(上例是proxy对象)进行操作,而不是针对目标对象(上例的空对象)进行操作

正是这样设置,所以才更加符合叫为 代理

使用细节:

  1. 可以将Proxy对象设置到object.proxy属性中,从而更好在object对象上调用

    var object={proxy:new Proxy(target,handler)};
    
  2. Proxy实例也可以作为其他对象的原型对象

    var proxy=new Proxy({},{
        get:function(target,property){
            return 35
        }
    })
    
    let obj=Object.create(proxy)
    obj.name //35
    
  3. 同一个拦截器函数可以设置拦截多个操作

    var handler={
        get:function(target,name){
            if(name === 'prototype'){
                return Object.prototype
            }
            return 'Hello,'+name
        },
        apply:function(target,thisBinding,args){
            return args[0]
        },
        construct:function(target,args){
            return {value:args[1]}
        }
    }
    

12.2 Proxy实例的方法

没有打上星号的请去查看MDN文档,MDN文档写的更好

12.2.1 get()

概述:get 方法用于拦截某个属性的读取操作

语法

get(target,propKey,receiver)

参数

  • target:目标对象
  • propKey:被获取的属性名。
  • receiver:Proxy 或者继承 Proxy 的对象

示例

1.拦截读取操作

var person={
    name:'老六'
}

var proxy=new Proxy(person,{
    get:function(target,property){
        if(property in target){
            return target[property]
        }else{
            throw new ReferenceError("该属性不存在")
        }
    }
})

console.log(proxy.name)
console.log(proxy.age)

上面的代码表示,如果访问目标对象不存在的属性,会抛出一个错误,如果没有这个拦截,访问不存在的属性只会返回undefined

2.get方法可以继承

let proto=new Proxy({},{
    get(target,property,receiver){
        console.log('获取原型的'+property)
        return target[property]
    }
})


let obj=Object.create(proto);
console.log(obj.name)
//获取原型的name

上面的代码中,拦截操作定义在Prototype对象上,所以如果读取obj对象继承的属性,拦截会生效

⭐️ 3.数组读取负数索引

function createArray(...element){
    let handler={
        get(target,property,receiver){
            let index=Number(property)
            if(index<0){
                property=String(target.length+index)
            }
            return Reflect.get(target,property,receiver)
        }
    }
    let target=[]

    target.push(...element)

    return new Proxy(target,handler)
}

let arr=createArray('a','b','c')
console.log(arr[-2]);
//'b'

⭐️ 4.实现属性的链式操作

<script>
    var pipe = (function () {
        return function (value) {
            var funcStack = [];
            var oproxy = new Proxy({}, {
                get: function (pipeObject, fnName) {
                    if (fnName === 'get') {
                        return funcStack.reduce(function (val, fn) {
                            return fn(val)
                        }, value)
                    }
                    //如果直接传入fnName是没有用的,只会单纯当作字符串去处理,实现不了上一步的fn(val) 函数调用,会报错
                    funcStack.push(window[fnName]); 
                    return oproxy
                }
            })

            return oproxy
        }
    }());


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


    console.log(pipe(3).double.pow.reverseInt.get)

script>

个人整理的实现思路:

  1. 因为要实现链式操作,和管道效果非常类似,数组中有一个方法reduce很相似,我们需要创建一个数组
  2. 每一次是依靠字符串,也就是方法名去实现的管道操作,但是实际上上,我们需要的是根据方法名调用函数,所以在数组中,我们要插入可以访问函数的东西的引用
  3. 终止符get,输出结果

4.生成DOM节点的通用函数dom

<script>

    const 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: 'www.baidu.com' }, '百度'),
        '. I like',
        dom.ul({},
            dom.li({}, 'The web'),
            dom.li({}, 'Food'),
            dom.li({}, '...actually that \'s it')
        )
    )

    document.body.appendChild(el)
script>

个人整理的实现思路:

  1. 首先要考虑函数中的根DOM节点,将这个确定的根节点挂载到body上,在上诉的代码实现中,采用了对象的属性形式去创建
  2. 标签元素的中的属性要考虑其中,所以第一个for表示传入的参数中提取标签属性
  3. 参数中可能含有标签或者标签内容,要进行判断
  4. 第二个for循环,将传入的多个参数标签依次插入根元素中

❗️ 注意:如果一个属性不可配置(configurable)或不可写(writable),则该属性不能被代理,通过Proxy对象访问该属性将报错

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

const handler1={
    get(target,property){
        return 'abc'
    }
};

const proxy1=new Proxy(target1,handler1);

console.log(proxy1.foo);
12.2.2 set() :

概述:方法用于拦截某个属性的赋值操作

语法

const p = new Proxy(target, {
  set: function(target, property, value, receiver) {
  }
});

参数

  • target:目标对象
  • property:将被设置的属性名。
  • value:新属性值
  • receiver:Proxy 或者继承 Proxy 的对象

返回值

  • 返回 true 代表属性设置成功。
  • 在严格模式下,如果 set() 方法返回 false,那么会抛出一个 TypeError 异常。

实例

1.有一个age属性,该属性应该是一个不大于200的整数

let validator={
    set:function(obj,prop,value){
        if(prop==='age'){
            if(!Number.isInteger(value)){
                throw new TypeError('这个值不是整数')
            }
            if(value>200){
                throw new RangeError('age属性值大于200')
            }
        }
        obj[prop]=value
    }
}

let person=new Proxy({},validator)
person.age=100
person.age //100
person.age='123' //报错
person.age=300 //报错

2.防止内部属性被外部读写

var 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(`无法${action}内部属性${key}`)
    }
}

var target={}

var proxy=new Proxy(target,handler)
proxy._prop='1'
//无法set内部属性_prop
console.log(proxy._prop);
//无法get内部属性_prop

❗️ 注意:如果目标对象的某个属性不可写也不可配置,那么set不得改变这个属性值,只能返回同样的值。否则报错

12.2.3 apply()

概述:拦截函数的调用,call和apply操作

语法

var p = new Proxy(target, {
  apply: function (target, thisArg, argumentsList) {},
});

参数

  • target:目标对象(函数)
  • thisArg:被调用时的上下文对象
  • argumentsList:被调用时的参数数组

实例

1.方法的基本使用

var target=function(){return '我是目标对象'}

var handler={
    apply:function(){
        return '我是代理对象'
    }
}

var p=new Proxy(target,handler)

console.log(p());

2.方法的进阶使用

var twice={
    apply(target,ctx,args){
        return Reflect.apply(...arguments) * 2;
    }
}

function sum(left,right){
    return left+right
}

var proxy=new Proxy(sum,twice)

console.log(proxy(1,2))
console.log(proxy.call(null,5,6))
console.log(proxy.apply(null,[7,8]))
12.2.4 has()

概念:拦截HasProperty操作,即判断对象是否具有某个属性,这个方法会生效,典型的操作就是in运算符

语法

var p = new Proxy(target, {
  has: function (target, prop) {},
});

参数

  • target:目标对象
  • prop:需要检查是否存在的属性

使用细节:

  1. has方法拦截的是HasProperty操作,而不是HasOwnProperty操作,即has方法不判断一个属性是对象自身的还是继承的属性
  2. 虽然for…in循环也用到了in运算符,但是has拦截不对其生效

实例

1.使用has方法隐藏某些属性,不被in方法发现

var handler={
    has(target,key){
        if(key[0] === '_'){
            return false
        }
        return key in target
    }
}

var target={
    _prop:'foo',
    prop:'bar'
}

var proxy=new Proxy(target,handler)
console.log('_prop' in proxy);
12.2.5 construct()

概念:用于拦截new命令

语法

var p = new Proxy(target, {
  construct: function (target, argumentsList, newTarget) {},
});

参数

  • target:目标对象
  • args:构建函数的参数对象

使用细节

  1. construct方法返回的必须是一个对象,否则报错

案例

var p = new Proxy(function () {}, {
  construct: function (target, argumentsList) {
    console.log("called: " + argumentsList.join(", "));
    return { value: argumentsList[0] * 10 };
  },
});

console.log(new p(1).value); // "called: 1"; outputs 10

12.2.7 defineProperty()

概念:拦截Object.defineProperty操作

语法

var p = new Proxy(target, {
  defineProperty: function (target, property, descriptor) {},
});

参数

  • target:目标对象
  • property:待检索其描述的属性名。
  • descriptor:待定义或修改的属性的描述符。有以下几种
    • enumerable:一个布尔值,指示属性是否可枚举。可枚举属性可以被 for...in 循环和 Object.keys() 方法枚举。默认为 false
    • configurable:一个布尔值,指示属性是否可配置。如果为 true,则可以使用 Object.defineProperty() 方法修改属性的描述符。如果为 false,则不能修改属性的描述符,也不能删除属性。默认为 false
    • writable:一个布尔值,指示属性是否可写。如果为 true,则属性的值可以被修改。如果为 false,则属性的值不可修改。默认为 false
    • value:默认为 undefined。当属性的 writable 特性为 true 时,可以修改属性的值。
    • get
    • set

返回值

defineProperty 方法必须以一个 Boolean 返回,表示定义该属性的操作成功与否。

使用细节:

  1. 这个方法生效的情况有以下几种,不止Object.definedProperty
    1. Object.definedProperty
    2. Reflect.definedProperty
    3. proxy.property=‘value’
  2. 如果目标对象不可扩展(extensible),则definedProperty不能增加目标对象中不存在的属性,否则会报错
  3. 如果目标对象的某个属性不可写(writable)或者不可配置(configurable) 则definedProperty方法不得改变这两个设置

实例:

var handler={
    defineProperty(target,key,descriptor){
        return false
    }
}

var target={};

var proxy=new Proxy(target,handler)
proxy.foo='bar'
//TypeError:proxy defineProperty handler returned false for property "foo"
12.2.9 getPrototypeOf()

概述: 拦截获取对象原型,具体来说,用于拦截以下操作

  • Object.prototype._ proto _
  • Object.prototype.PrototypeOf()
  • Object.getPrototypeOf()
  • Reflect.getPrototypeOf()
  • instanceof
12.2.11 ownKeys()

ownKeys方法用来拦截对象自身属性的读取操作,具体来说,拦截以下操作

  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.keys()

使用细节:

  1. 使用Object.keys方法时,有三类属性会被ownKeys方法自动过滤,不会返回
    1. 目标对象不存在的属性
    2. 属性名为Symbol值
    3. 不可遍历(enumerable) 的属性
  2. ownKeys方法返回数组成员只能是字符串或Symbol值 如果有其他类型的值,或者返回的根本不是数组,就会报错
  3. 如果目标对象自身包含不可配置的属性,则该属性必须被ownKeys方法返回,否则会报错

实例:

1. 拦截Object.keys()的例子

let target={
    a:1,
    b:2,
    c:3
}

let handler={
    ownKeys(target){
        console.log('输出啦')
        return ['a']
    }
}

let proxy=new Proxy(target,handler)

console.log(Object.keys(proxy))

//输出啦
//[ 'a' ]

2. 目标对象自身包含不可配置的属性,会报错

var obj={}
Object.defineProperty(obj,'a',{
    configurable:false,
    enumerable:true,
    value:10
})

var p=new Proxy(obj,{
    ownKeys:function(target){
        return ['b']
    }
})

console.log(Object.getOwnPropertyNames(p));
//TypeError: 'ownKeys' on proxy: trap result did not include 'a'

12.3 Proxy.revocable()

proxy.revocable方法返回一个可取消的Proxy实例

let target1={};
let handler1={};

let {proxy,revoke}=Proxy.revocable(target,handler)

proxy.foo=123
console.log(proxy.foo)
//123

revoke()
console.log(proxy.foo);
//TypeError: Cannot perform 'get' on a proxy that has been revoked

上述代码中,Proxy.revocable方法返回一个对象,其proxy属性是Proxy实例,revoke属性是一个函数,可以取消proxy实例,上面的代码中,当执行revoke函数后访问Proxy实例,就会抛出一个错误。

12.4 this问题

虽然Proxy可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下也无法保证与目标对象的行为一致

主要的原因就是在Proxy代理的情况下,目标对象内部的this关键字会指向Proxy代理

this指向会引起下面两种情况的实例:

  1. 由于this指向问题的变化导致Proxy无法代理目标对象

    const _name=new WeakMap()
    
    class Person{
        constructor(name){
            _name.set(this,name)
    
        }
    
        get name(){
            return _name.get(this)
        }
    }
    
    const jane=new Person('Jane')
    console.log(jane.name);
    //'Jane'
    
    const proxy=new Proxy(jane,{});
    console.log(proxy.name);
    //undefined
    
  2. 原生对象的内部属性只有通过正确的this才能获取

    const target=new Date()
    const handler={}
    const proxy=new Proxy(target,handler)
    
    console.log(proxy.getDate())
    //TypeError:this is not a Date object
    

    上面的代码中,getDate方法只能在Date对象实例上面获取,如果this不是Date对象实例就会报错,这时,this绑定原始对象就可以解决这个问题。

    const target=new Date('2015-01-01')
    const handler={
        get(target,prop){
            if(prop === 'getDate'){
                return target.getDate.bind(target)
            }
            return Reflect.get(target,prop)
        }
    }
    
    const proxy=new Proxy(y=target,handler)
    
    proxy.getDate() //1
    

你可能感兴趣的:(JavaScript进阶指南,javascript,es6,前端)