你应该掌握的JavaScript高阶技能(三)

Proxy 学习

朋友们,大家好!本期内容是《你应该掌握的 JavaScript 高阶技能》的第三期,内容主要是 Proxy 代理 ,关于第一和第二期遗留内容 typeScript 实战项目贪吃蛇和手写 Promise 会在后续继续更新!本期参考 JavaScript 高级程序设计(第四版)阮一峰老师的《ECMAScript 6 标准入门》,特别鸣谢❤️!

ES6 新增的反射内容将于新一期讲解,本文不多赘述。



1. Proxy 概述


  • Proxy (代理)可以给目标对象定义一个关联的代理对象,而这个代理对象可以作为抽象的目标对象来使用。在对目标对象的各种操作影响目标对象之前,可以在代理对象中对这些操作加以控制。

  • Proxy 是一个构造函数,可以通过它生成一个 Proxy 实例。

  • 跟 JavaScript 中的 Object.defineProperty 类似(也就是访问器属性,关注 vue2.x 的朋友都知道,vue2.x 的底层响应式原理部分就是使用它来实现的,vue3.x 使用的是 Proxy + Reflect 来实现的数据代理)

  • Proxy (代理) 用于创建一个对象的代理,可以在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。


2. Proxy 使用


  • 语法 new Proxy(target,handler)
    • target : 目标对象,待操作对象(可以是普通对象,数组,函数,proxy 实例对象)
    • handler : 一个通常以函数作为属性的对象,也可以是空对象,各属性中的函数分别定义了在执行各种操作时代理 Proxy 对象的行为。
  • 生成的实例对象是针对 target 对象的“拦截器”。
let obj = {
    name: 'zhangsan',
    age: 20
}
let hander = {};
const p = new Proxy(obj, hander);
console.log(p);

在这里插入图片描述

  • 上图示可知 proxy 对象中,包含了 Handler 属性和 Target 属性和 IsRevoked 属性,值分别是传入的 handler 以及 obj 和 false。
  • isRevoked 表示是否可撤销,生成可撤销的 proxy 对象用Proxy.revocable() 方法。
  • Proxy 还有一种使用方式:Proxy.revocable 的静态方法,具体细节在后续说明。
const target = {};
 
const {proxy, revoke} = Proxy.revocable(target, {
     get (target, key, receiver) {
         return 123;
     }
 })
 
console.log(proxy.getData); // 123

2.1 通过 Proxy 对象操作原对象


  • 2.1.1 空代理

  • 最简单就是空代理,也就是 handler 是个空对象,那么操作拦截器相当于直接操作目标对象 target。
  • 要创建空代理,可以传入一个简单的对象字面量作为处理程序对象。
  • 在代理对象 proxy 上执行的任何操作都会应用到目标对象 target 上。
//定义目标对象
let obj = {
    name: 'zhangsan',
    age: 20
}
//定义拦截器
let hander = {};
const p = new Proxy(obj, hander);
//读取属性 修改属性
console.log(p.name);//zhangsan

p.age = 18;
console.log(p.age);//18
console.log(obj.age);//18

p.address = 'xxx';
console.log(p.address);
console.log(p);

delete p.age;
console.log(p.age);//undefined
console.log(obj.age);//undefined
  • 操作 proxy 实例对象致使原对象的属性也被修改

  • 2.1.2 Object.defineProperty 实现数据劫持
let o = {
    name: 'lisi',
    age: 20
};
let temp = o['name'];
Object.defineProperty(o,'name',{
    get(){
        console.log('name被读取了');
        return temp;
    },
    set(newValue){
    	temp = newValue;
        console.log('name被修改为' + newValue);
	}

});

在这里插入图片描述

  • 2.1.3 Proxy 拦截读取/修改属性行为的实例

  • 同一个拦截器函数,可以设置拦截多个操作。
let o = {
    name: 'lisi',
    age: 20
};
const proxy = new Proxy(o,{
    get: function(target,propKey){
        console.log(`${propKey}属性被读取`);
        return target[propKey];
    },
    set: function(target,propKey,newValue){
        console.log(`${propKey}属性被修改`);
        target[propKey] = newValue;
	}
});
//通过代理对象执行 get() / set() 操作时,就会触发定义的 get() / set() 拦截器,该操作可以通过多种形式触发并被 get() / set() 拦截器拦截,后续会讲。
//这些操作只要发生在代理对象上 proxy,就会触发 get() / set() s拦截器,注意在代理对象上执行相应的形式操作才会触发拦截器,在目标对象上执行仍会产生相应的行为。 
  • 上面代码中,handler 对象中有一个 get 方法,用来拦截对目标对象属性的访问请求get 方法的两个参数分别是目标对象和所要访问的属性,set 方法的三个参数分别是目标对象、所要访问的属性和所设置的新值,用来拦截目标对象属性的设置

在这里插入图片描述


  • 2.1.4 Proxy 作为其他对象的原型
var proxy = new Proxy({}, {
  get: function(target, propKey) {
    return 1;
  }
});

let obj = Object.create(proxy);
/*	
	1.Object.create() 创建的新对象的原型指向接收的参数本身,
	2.new Object() 创建的新对象的原型指向的是 Object 的prototype。
	3.可以通过 Object.create(null) 创建一个没有原型的对象
	4.new Object() 创建的对象是 Object 的实例,原型永远指向Object 的 prototype。
*/
console.log(obj);
console.log(obj.male);//1

在这里插入图片描述

  • 上面代码中,proxy 对象是 obj 对象的原型,obj 对象本身并没有 male 属性,所以根据原型链,会在 proxy 对象上读取该属性,会被拦截。

3. Proxy 支持的拦截方法操作


  • 3.1 get(target, propKey, receiver)

  • 拦截对象属性的读取
    • 拦截器参数
      • target:目标对象
      • propKey:引用的目标对象上的字符串键属性或符号键
      • receiver:代理对象或继承代理对象的对象
    • 拦截操作:
      • proxy.foo
      • proxy['foo']
      • Object.create(proxy)['foo']
    • target.propKey 是不可配置(configurable:表示能否通过 delete 删除属性、能否修改属性的特性)且不可写;(writable:表示能否修改属性的值,即值是可写的还是只读。),则 Proxy 不能修改该属性,否则通过 Proxy 对象访问该属性会报错。
  • 利用 Proxy,可以将读取属性的操作(get),转变为执行某个函数,从而实现属性的链式操作
let pipe = function(value){
    //pipe 类型是 proxy
    let funcArr = [];
    let p = new Proxy({},{
        //拦截 pipe.foo 操作  
        get: function(target,propKey){
            //拦截属性为 get 
            if(propKey === 'get'){
                //循环执行 funArr 里面的方法
                return funcArr.reduce(function (val,fn){
                    return fn(val);
                },value);
            }
            //拦截属性不为 get 存储 [window] 下对应方法名的函数
            funcArr.push(window[fnName]);
            return p;
        }
    });
    return p;
}
//不能是 let const
var double = n => n*2;
//double(3) //6
var pow = n => n*n;
//pow(5) //25
var reverseInt = n => n.toString().split("").reverse().join("") | 0;
//reverseInt(1234) //4321
//把数字转换为字符串;把字符串分割成字符串数组;翻转数组;将数组作为字符串返回
console.log(pipe(4).double.pow.reverseInt.get);//46
  • 下面是一个get方法的第三个参数的例子,它总是指向原始的读操作所在的那个对象,一般情况下就是 Proxy 实例。
const proxy = new Proxy({}, {
  get: function(target, key, receiver) {
    return receiver;
  }
});
proxy.getReceiver === proxy // true
  • 上面代码中,proxy 对象的 getReceiver 属性是由 proxy 对象提供的,所以receiver指向 proxy 对象。

  • 3.2 set(target, propKey, value, receiver)

  • 拦截对象属性的设置
    • 返回 true 表示成功,返回 false 表示失败,严格模式下会抛出 TypeError。
    • 拦截器参数
      • target:目标对象
      • propKey:引用的目标对象上的字符串键属性或符号键
      • value: 要赋给属性的值
      • receiver:接收最初赋值的对象(一般情况下是 proxy 实例对象本身)
    • 拦截操作:
      • proxy.foo = value
      • proxy['foo'] = value
      • Object.create(proxy)['foo'] = value
    • 如果目标对象自身的某个属性 target.propKey 不可写,那么 set 方法将不起作用。
const handler = {
  set: function(obj, prop, value, receiver) {
    obj[prop] = receiver;
    return true;
  }
};
const proxy = new Proxy({}, handler);
const myObj = {};
Object.setPrototypeOf(myObj, proxy);

myObj.foo = 'bar';
myObj.foo === myObj // true
  • 上面代码中,设置 myObj.foo 属性的值时,myObj 并没有 foo 属性,因此引擎会到 myObj 的原型链去找 foo 属性。myObj 的原型对象 proxy 是一个 Proxy 实例,设置它的 foo 属性会触发 set 方法。这时,第四个参数 receiver 就指向原始赋值行为所在的对象 myObj。

  • 3.3 has(target, propKey)

  • 返回必须是布尔类型,非布尔类型会被转型为布尔类型。
  • 拦截操作:
    • propKey in proxy
    • propKey in Object.create(proxy)
  • 判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in 运算符
    • has 方法接收两个参数:target:目标对象,propKey:需要查询的属性名.
    • has 方法拦截的是 HasProperty 操作而不是 HasOwnProperty 操作,也就是说 has 方法不判断一个属性是本身的属性还是继承来的属性
    • 如果原对象不可配置或者禁止扩展(Object.preventExtensions()),has 方法拦截会报错。
  • 隐藏对象的某些属性不被 in 发现,比如以 $ 符号开头的属性
const target = { _data: 'bar', $data: 'baz', _el: '<div></div>', $el: '#app' };

const handler = {
    has(target,propKey){
        if (propKey[0] === '$') {
            return false;
        }
        console.log(target[propKey]);
        return propKey in target;
    }
}
const p = new Proxy(target,handler);

在这里插入图片描述

  • for...in循环也用到了in运算符,但是 has 方法拦截对其生效。
let stu1 = {name: '张三', gender: '男'};
let stu2 = {name: '李四', gender: '女'};

const handler = {
  has(target, propKey) {
    if (propKey === 'gender' && target[propKey] === '男') {
        return false;
    }
    console.log(target[propKey]);
    return propKey in target;
  }
}

const p1 = new Proxy(stu1, handler);
const p2 = new Proxy(stu2, handler);
for (let k in p1) {
    console.log(p1[k]);
}
for (let k in p2) {
    console.log(p2[k]);
}

在这里插入图片描述


  • 3.4 deleteProperty(target, propKey)
  • 必须返回布尔值,表示属性是否被删除成功。如果这个方法抛出错误或者返回 false,当前属性就无法被delete命令删除。返回非布尔值会被转型为布尔值。
  • 拦截 delete 操作
    • delete proxy[propKey]
    • delete proxy.propKey
  • 目标对象自身的不可配置(configurable)的属性,不能被deleteProperty方法删除,否则报错。
//invariant 恒常,不可变
function invariant(key,action){
    if(key[0] === '$'){
        throw new Error(`Invalid attempt to ${action} private "${key}" property`);
    }
}
const handler = {
    deleteProperty(target,propKey){
        invariant(propKey,'delete');
        delete target[propKey];
        return true;
    }
};
//deleteProperty 方法拦截了 delete 操作符
const target = {
    $el:'#app',
    _el:'<div></div>',
};
const p = new Proxy(target,handler);

在这里插入图片描述


  • 3.5 ownKeys(target)

  • 用来拦截对象自身属性的读取操作
  • 返回一个数组,数组成员只能是字符串或 Symbol 值。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • 拦截操作:
    • Object.getOwnPropertyNames(proxy)
    • Object.getOwnPropertySymbols(proxy)
    • Object.keys(proxy)
    • for...in循环
// ownKeys() 方法拦截 Object.getOwnPropertyNames()
let p = new Proxy({}, {
  ownKeys: function(target) {
    return ['a', 'b', 'c'];
  }
});

Object.getOwnPropertyNames(p);
// ['a','b','c']
let target = {
  a: 1,
  b: 2,
  c: 3
};

let handler = {
  ownKeys(target) {
    return ['a'];
  }
};

let proxy = new Proxy(target, handler);
Object.keys(proxy);// [ 'a' ]
  • 上面代码拦截了对于 target 对象的 Object.keys() 操作,只返回 abc 三个属性之中的 a 属性。

  • 注意,使用Object.keys()方法时,有三类属性会被ownKeys()方法自动过滤,不会返回。

    • 目标对象上不存在的属性
    • 属性名为 Symbol 值
    • 不可遍历(enumerable)的属性
  • ownKeys() 方法返回除字符串或 Symbol 其他类型的值,或返回的表示数组,会报错

let obj = {}
let p = new Proxy(obj,{
    ownKeys: function(target) {
        return [123,true,undefined,null,{},[]];
	}
});
Object.getOwnPropertyNames(p);
// Uncaught TypeError: 123 is not a valid property name...
let target = {
  a: 1,
  b: 2,
  c: 3,
  [Symbol.for('secret')]: '4',
  //Symbol.for(key) 根据给定的键 key,来从运行时的 symbol 注册表中找到对应的 symbol,如果找到了,则返回它,否则,新建一个与该键关联的 symbol,并放入全局 symbol 注册表中。
};

Object.defineProperty(target, 'key', {
  enumerable: false,
  configurable: true,
  writable: true,
  value: 'static'
});

let handler = {
  ownKeys(target) {
    return ['a', 'd', Symbol.for('secret'), 'key'];
  }
};

let proxy = new Proxy(target, handler);

Object.keys(proxy)
// ['a']
  • 上面代码中,ownKeys() 方法之中,显式返回不存在的属性(d)、Symbol 值(Symbol.for('secret'))、不可枚举的属性(key),结果都被自动过滤掉。

  • 如果目标对象自身包含不可配置的属性,则该属性必须被 ownKeys() 方法返回,否则报错。

var obj = {};
Object.defineProperty(obj, 'a', {
  configurable: false,//不可配置
  enumerable: true,
  value: 10 }
);

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

Object.getOwnPropertyNames(p)
// Uncaught TypeError: 'ownKeys' on proxy: trap result did not include 'a'
  • 上面代码中,obj对象的 a 属性是不可配置的,这时 ownKeys() 方法返回的数组之中,必须包含 a,否则会报错

  • 如果目标对象是不可扩展的(non-extensible),这时 ownKeys() 方法返回的数组之中,必须包含原对象的所有属性,且不能包含多余的属性,否则报错。

var obj = {
  a: 1
};

Object.preventExtensions(obj);

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

Object.getOwnPropertyNames(p);
// Uncaught TypeError: 'ownKeys' on proxy: trap returned extra keys but proxy target is non-extensible
  • 上面代码中,obj 对象是不可扩展的,这时 ownKeys() 方法返回的数组之中,包含了obj 对象的多余属性 b,所以导致了报错。

  • 3.6 getOwnPropertyDescriptor(target, propKey)

  • 拦截 Object.getOwnPropertyDescriptor(proxy, propKey),返回必须时属性的描述对象,或者在属性不存在时返回 undefined。

  • Object.getOwnPropertyDescriptor(obj, prop) 获取指定对象指定的自有属性的属性描述符。

  • 在这里插入图片描述

  • 注意: target.propKey 【存在/不存在】 和 【可配置/不可配置】 的情况。

const target = {
    _el: '<div></div>',
    $el: '#app'
}
const handler = {
    getOwnPropertyDescriptor(target, PropKey) {
        if (PropKey[0] === '$') {
            return false;
        }
        return Object.getOwnPropertyDescriptor
(target, PropKey);
    }
}

在这里插入图片描述

上面代码中,handler.getOwnPropertyDescriptor() 方法对于第一个字符为 $ 的属性名会返回 undefined


  • 3.7 defineProperty(target, propKey, descriptor)

  • 拦截Object.defineProperty(proxy, propKey, descriptor),必须返回一个布尔值,返回非布尔值会被转型为布尔值。
  • 拦截器参数
    • target:目标对象
    • propKey:引用的目标对象上的字符串键属性或符号键
    • descriptor: 包括可选的enumerable、configurable、writable、value、get 和 set定义的对象。
  • 注意:
    • 如果目标对象不可扩展,则无法定义属性。
    • 如果目标对象的某个属性不可写或不可配置的,则defineProperty()方法不得改变这两个设置。
const handler = {
  defineProperty (target, key, descriptor) {
    return false;
  }
};
const target = {};
const p = new Proxy(target, handler);

在这里插入图片描述

  • 上面代码中,defineProperty()方法内部没有任何操作,只返回false,导致添加新属性总是无效。注意,这里的false只是用来提示操作失败,本身并不能阻止添加新属性。

  • 3.8 preventExtensions(target)

  • 拦截Object.preventExtensions(proxy),必须返回布尔值,表示 target 是否已经不可扩展,返回非布尔值会被转型成布尔值。
  • 如果目标对象不可扩展时(即 Object.isExtensible(proxy)false),proxy.preventExtensions 才能返回 true,否则会报错。
const p = new Proxy({}, {
  preventExtensions: function(target) {
    return true;
  }
});

Object.preventExtensions(p);
// Uncaught TypeError: 'preventExtensions' on proxy: trap returned truish but the proxy target is extensible
  • 为了防止出现这个问题,通常要在 proxy.preventExtensions()方法里面,调用一次 Object.preventExtensions()
const p = new Proxy({}, {
  preventExtensions: function(target) {
    Object.preventExtensions(target);
    //此时 Object.isExtensible(p) 为 false
    return true;
  }
});

Object.preventExtensions(p);

  • 3.9 getPrototypeOf(target)

  • 返回一个对象或 null。
  • 拦截操作
    • Object.prototype.__proto__
    • Object.prototype.isPrototypeOf(proxy)
    • Object.getPrototypeOf(proxy)
    • Reflect.getPrototypeOf(proxy)
    • proxy instanceof Object
  • 如果目标对象不可扩展, Object.getPrototypeOf(proxy)方法必须返回目标对象的原型对象(Object.getPrototypeof(target))。
const proto = {};
const p = new Proxy({}, {
  getPrototypeOf(target) {
    return proto;
  }
});
Object.getPrototypeOf(p) === proto // true

  • 3.10 isExtensible(target)

  • 拦截 Object.isExtensible(proxy),必须返回一个布尔值,表示 target 是否可扩展,返回非布尔值会被转型成布尔值。
const p = new Proxy({}, {
  isExtensible: function(target) {
    return true;
  }
});

Object.isExtensible(p)
// true
  • target 可扩展,则处理程序必须返回 true,反之不可扩展,返回 false,也就是说它的返回值必须与目标对象的 isExtensible 属性保持一致,否则就会抛出错误
// Object.isExtensible(proxy) === Object.isExtensible(target)

const p = new Proxy({}, {
  isExtensible: function(target) {
    return false;
  }
});

Object.isExtensible(p)
// Uncaught TypeError: 'isExtensible' on proxy: trap result does not reflect extensibility of proxy target (which is 'true')

  • 3.11 setPrototypeOf(target, proto)

  • 拦截 Object.setPrototypeOf(proxy, proto),返回一个布尔值,表示原型赋值是否成功,返回非布尔值会被转型成布尔值。
  • 参数:
    • target:目标对象
    • proto:target 的替代原型,如果是顶级原型则为 null
  • 如果目标对象不可扩展,setPrototypeOf()方法不得改变目标对象的原型,则唯一有效的 prototype 参数就是 Object.getPrototypeOf(target) 的返回值。
const target = {};
const handler = {
    setPrototypeOf(target, prototype) {
        console.log(`target: ${target},prototype: 
${prototype}`);
        console.log(`setPrototypeOf()`);
        return true;
    }
}
const p = new Proxy(target, handler);

在这里插入图片描述


  • 3.12 apply()

  • 拦截 Proxy 实例作为函数调用的操作。
  • 接受三个参数,分别是目标对象(target)、目标对象的上下文对象(this)和目标对象的参数数组(args)。
  • 拦截操作
    • proxy(...args)
    • Function.prototype.call(thisArg, ...args)
    • Function.prototype.apply(target,thisArg,args)
const target = (a, b) => a + b;
const handler = {
    apply(target, thisArg, args) {
        console.log('apply()');
        console.log(`target: ${target}`);
        console.log(`thisArg: ${thisArg}`);
        console.log(`args: ${args}`);
        return target(...args);
    }
}
const p = new Proxy(target, handler);

在这里插入图片描述


  • 3.13 construct(target, args)

  • 拦截 Proxy 实例作为构造函数调用的操作。
  • 返回值必须是一个对象,否则会报错。
  • 拦截 new 命令
    • new proxy(...args)
  • construct() 方法可以接受三个参数。
    • target:目标构造函数
    • args:构造函数的参数数组。
    • newTarget:创造实例对象时,new 命令作用的构造函数
const target = function () { };
const handler = {
    construct(target, args) {
        console.log(this === handler);
        return new target(...args);
    }
}
const p = new Proxy(target, handler);

在这里插入图片描述

const target = function () { };
const sum = function (total, sum) {
    return total + sum;
}
const handler = {
    construct(target, args) {
        console.log(args);
        return {
            value: args.reduce(sum)
        }
    }
}
const p = new Proxy(target, handler);

在这里插入图片描述


4. 可撤销代理


  • 有时候可能需要中断代理对象与目标对象之间的联系。对于使用 new Proxy() 创建的普通代理来说,这种联系会在代理对象的生命周期一直存在。
  • Proxy.revocable() 方法,返回一个可取消的 Proxy 实例。支持撤销代理对象与目标对象的关联。撤销代理的操作是不可逆的。
let target = {
    foo: 'bar'
};
let handler = {};
let { proxy, revoke } = Proxy.revocable(target, handler);
//解构

在这里插入图片描述

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


5. 代理中的 this


  • 一般来说,方法中的 this 通常指向调用这个方法的对象
  • Proxy 拦截函数内部的this,指向的是handler对象。
const target = {
    this() {
        return this === proxy 
    }
};
const handler = {};
const proxy = new Proxy(target,handler);
console.log(proxy.this);//true
console.log(target.this);//false
  • 上面代码中,一旦 proxy 代理 target,target.this()内部的 this就是指向 proxy ,而不是target。所以,虽然 proxy 没有做任何拦截,target.this()proxy.this()返回不一样的结果。
  • 但是如果目标对象依赖于对象标识,可能会出现一些问题。
const wm = new WeakMap();

class User {
  constructor(userId) {
    wm.set(this, userId);
  }
  get id() {
    return wm.get(this);
  }
  set id(userId) {
    wm.set(this, userId);
  }
}

const user = new User(123);
console.log(user.id) // 123

const proxy = new Proxy(user, {});
console.log(proxy.id) // undefined
  • 上面代码中,目标对象 useruserId 属性,实际保存在外部 WeakMap 对象 wm上面,通过 this 键区分。由于通过 proxy.id 访问时,this指向 proxy,导致无法取到值,所以返回undefined

  • 此外,有些原生对象的内部属性,只有通过正确的this才能拿到,所以 Proxy 也无法代理这些原生对象的属性。

const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);
proxy.getDate();// TypeError: this is not a Date object.
const target = {
    getDate: new Date('2022-11-20'),
    a: 1,
    b: 2,
    c: 3
};
const handler = {
    get(target, prop) {
        if (prop === 'getDate') {
            return target[prop].getDate.bind(target
[prop]);
        }
        return target[prop];
    }
};
const proxy = new Proxy(target, handler);
console.log(proxy.getDate()) // 20
  • 上面代码中,getDate() 方法只能在 Date 对象实例中使用,如果this 不是 Date 对象实例就会报错。这时,this 绑定到 target[prop],就可以解决这个问题。

6. 小结


Proxy 代理应用场景是非常丰富的,比如跟踪属性访问、隐藏属性、阻止修改或删除属性、函数参数验证、构造函数参数验证、数据绑定和可观察对象等等,细心的朋友可能发现,枚举的部分应用场景在本文中实现了,未实现的举例大家可以尝试着去完成哦!

不得不说代理是 ES6 新增的令人兴奋和动态十足的新特性,尽管不支持向后兼容,但其开辟出了一片前所未有的 JavaScript 元编程及抽象的新天地!


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值