如果说常规编程是写代码去操作数据,那么元编程就是写代码去操作其他代码。
1 属性的特性
JS的属性有名字和值,但每个属性也有3个关联的特性:
可写(writable)特性指定是否可修改属性的值。
可枚举(enumerable)特性指定是否可以通过for/in循环和Object.keys()方法枚举属性。
可配置(configurable)特性指定是否可以删除属性,以及是否可以修改属性的特性。
这些特性允许库作者给原型对象添加方法,并让它们像内置方法一样不可枚举;允许作者"锁住"自己的对象,定义不能修改或删除的属性。
1.1 访问器属性和数值属性
访问器属性(get或set方法)没有value及writable特性,有4个特性:get、set、enumerable和configurable。
数值属性的4个特性是:value、writable、enumerable和configurable。
Object有以下方法来操作这些特性:
-
Object.getOwnPropertyDescriptor() 获取特定对象某个属性的属性描述符。
-
Object.defineProperty() 创建属性并可定义属性描述符。Object.defineProperties() 则可同时定义多个属性及其描述符。
let obj = {
get name() { return 'obj' },
x: 1
}
Object.defineProperty(obj,"y",{
value: 1,
writable: false,
enumerable: true,
configurable: false
})
Object.defineProperties(obj,{
z: {value: 1, writable: true},
age: { get: () => 18 }
})
console.log(obj);
console.log(Object.getOwnPropertyDescriptor(obj,"name"));
console.log(Object.getOwnPropertyDescriptor(obj,"z"));
console.log(Object.getOwnPropertyDescriptor(obj,"age"));
// { name: [Getter], x: 1, y: 1 }
// {
// get: [Function: get name],
// set: undefined,
// enumerable: true,
// configurable: true
// }
// { value: 1, writable: true, enumerable: false, configurable: false }
// {
// get: [Function: get],
// set: undefined,
// enumerable: false,
// configurable: false
// }
1.2 自定义深度复制函数
Object.assign()只复制可枚举属性和属性值,但不复制属性特性。这意味着如果源对象有个访问属性,那么复制到目标对象的是获取函数的返回值。
let obj = {}
Object.defineProperties(obj,{
x: { value: 1, enumerable: true ,writable: false},
y: { value: 2, enumerable: false, writable: false},
name: { get: () => "obj",enumerable: true }
})
let target = {}
Object.assign(target, obj);
console.log(target);
console.log(Object.getOwnPropertyDescriptor(target,"x"));
console.log(Object.getOwnPropertyDescriptor(target, "name"))
// { x: 1, name: 'obj' }
// { value: 1, writable: true, enumerable: true, configurable: true }
// { value: 'obj', writable: true, enumerable: true, configurable: true }
我们自定义个深度复制函数。从一个对象向另一个对象复制属性及它们的特性。
Object.defineProperty(Object, "assignDeep", {
writable: false,
enumerable: false,
configurable: false,
value: function (target, ...sourceList) {
for (let source of sourceList) {
for (let name of Object.getOwnPropertyNames(source)) {
let desc = Object.getOwnPropertyDescriptor(source,name);
Object.defineProperty(target,name,desc);
}
for (let symbol of Object.getOwnPropertySymbols(source)) {
let desc = Object.getOwnPropertyDescriptor(source, name);
Object.defineProperty(target,name,desc);
}
}
}
})
let target = {};
let source = { x: 1};
Object.defineProperties(source, {
y: { value: 2, enumerable: false},
name: {get: () => "source"},
z: { value: 3, enumerable: true}
});
Object.assignDeep(target,source);
console.log(target);
console.log(Object.getOwnPropertyDescriptor(target,"y"));
console.log(Object.getOwnPropertyDescriptor(target,"name"));
// { x: 1, z: 3 }
// { value: 2, writable: false, enumerable: false, configurable: false }
// {
// get: [Function: get],
// set: undefined,
// enumerable: false,
// configurable: false
// }
1.3 对象的可扩展能力
对象的可扩展特性控制是否可以给对象添加新属性,即是否可扩展。
Object.isExtensible() 判断一个对象是否可扩展,
Object.preventExtensions() 让对象不可扩展,只会影响对象本身,而不会影响其原型。
把对象改为不可扩展是不可逆的。
2 公认符号
是Symbol()工厂函数的一组属性,也就是一组符号值。通过这些符号值,我们可以控制js对象和类的某些底层行为。
1)hasInstance,如果instanceof 的右侧是一个有[Symbol.hasInstance]方法的对象,那么就会以左侧的值作为参数来调用这个方法并返回这个方法的值。
let bigNum = {
[Symbol.hasInstance](v) {
return v > 100
}
};
console.log(12 instanceof bigNum); // false
console.log(124 instanceof bigNum); // true
2)toStringTag, 在调用对象的toString方法时,会查找自己的参数中是否有一个属性的符号名是Symbol.toStringTag,如果有,则使用这个属性的值作为蔬菜。
class People {
get [Symbol.toStringTag]() {
return "People 人";
}
}
class Student {
}
console.log(Object.prototype.toString.call(new Student())); // [object Object]
console.log(new Student()); // {}
console.log(Object.prototype.toString.call(new People())); // [object People 人]
console.log(new People()); // People [People 人] {}
- 模式匹配符号,match()、matchAll()、search()、replace()和split()这些字符串方法中的任意一个,都有一个与之相对应的公认符号。
class RegExtCustom {
constructor(glob) {
this.glob = glob;
}
[Symbol.search](s) {
return s.search(this.glob);
}
}
let reg = new RegExtCustom("[12]2");
console.log("1234".search(reg)); // 0
console.log("ad32".search(reg)); // -1
3 模版标签
位于反引号直接的字符串被称为"模版字面量"。如果一个函数的表达式后面跟着一个模版字面量,那就会转换为一个函数调用。
这个函数第一个参数是一个字符串数组,然后是0或多个额外任何类型的参数。如果模版字面量包含一个要插入的值,那么字符串数组就会收到2个参数,表示这个被插入值两边的字面量。(如果要插入2个值,则字符串数组会收到3个参数)。
function templateFun(strArr,...args) {
console.log("字符串数组参数:",strArr);
console.log("插入参数", args);
}
let num = 999;
let str = 'hello word';
templateFun`&{num}{str}&`;
// 字符串数组参数: [ '&', '', '&' ]
// 插入参数 [ 999, 'hello word' ]
4 反射与代理对象
ES6及之后版本中的Proxy类是JS中最强大的元编程特性。使用它可以修改JS对象的基础行为。
4.1 反射API
Reflect不是类而是对象,它的属性只是定义了一组相关函数。
1)apply(f,o,arg),将函数f作为o的方法进行调用(如果o是null,则调用函数f时没有this值),并传入args数组的值作为参数。
function fun(...args) {
console.log(this.toString(),args);
}
let obj = {
get [Symbol.toStringTag]() {
return "自定义obj";
}
}
Reflect.apply(fun,obj,["hello","js"]); // [object 自定义obj] [ 'hello', 'js' ]
-
getPrototypeof(o), 返回对象o的原型,如果没原型则返回null。
-
set(o,name,value,receiver), 根据指定的name将对象o的属性值指定为value,如果指定了receiver参数,则将设置方法作为receiver和非o对象的设置方法调用。
class Student {
set name(val) {
console.log("设置名字:",val)
}
}
let student = new Student();
student.name = "hello student"; // 设置名字: hello student
Reflect.set(student,"name", "你好啊",(val) => {
console.log("反射设置值",val);
}); // 设置名字: 你好啊
4.2 代理对象
let proxy = new Proxy(target,handlers); // target 目标对象 handlers处理器对象。 proxy代理对象。
代理对象没有自己的状态或行为,每次对它执行某个操作,它只会把相应的操作发送给处理器对象或目标对象。(如果处理器对象上存在对应方法,代理就调用该方法,否则执行目标对象对应的方法)。
function creteCustomProxy(target) {
let handlers = {
get(target,property, receiver) {
console.log(`Handler GET target:${target} property: ${property}`);
return Reflect.get(target,property,receiver);
},
set(target,prop, value, receiver) {
console.log(`Handler SET target ${target} prop ${prop} value: ${value}`);
if (prop === 'java') throw new Error("不是JAVA");
Reflect.set(target,prop,value,receiver);
}
};
return new Proxy(target,handlers);
}
let obj = {};
let proxy = creteCustomProxy(obj);
proxy.x = "hello";
console.log(proxy.x);
proxy.java = "haha";
// Handler SET target [object Object] prop x value: hello
// Handler GET target:[object Object] property: x
// hello
// Handler SET target [object Object] prop java value: haha
// /Users/huangzaizai/Desktop/个人/代码/js-study/day4/article/s10.js:9
// if (prop === 'java') throw new Error("不是JAVA");
// ^
//
// Error: 不是JAVA
4.2.1 防御性编程
Proxy.revocable() 函数返回一个对象,包含代理对象和一个revoke()函数,一旦调用revoke(),代理立即失效。
let obj = {}
let handle = {}
let { proxy, revoke } = Proxy.revocable(obj,handle);
proxy.x = 2;
console.log(obj); // { x: 2 }
revoke();
proxy.y = 2; // 报错 TypeError: Cannot perform 'set' on a proxy that has been revoked
这个函数的用处是:如果必须向一个不受自己控制的库传一个函数,则可以给它传一个可撤销代理,在使用完这个库之后撤销代理,这样可以防止第三方库持有对你函数的引用。