JS元编程

如果说常规编程是写代码去操作数据,那么元编程就是写代码去操作其他代码。

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有以下方法来操作这些特性:

  1. Object.getOwnPropertyDescriptor() 获取特定对象某个属性的属性描述符。

  2. 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 人] {}

  1. 模式匹配符号,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' ]

  1. getPrototypeof(o), 返回对象o的原型,如果没原型则返回null。

  2. 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

这个函数的用处是:如果必须向一个不受自己控制的库传一个函数,则可以给它传一个可撤销代理,在使用完这个库之后撤销代理,这样可以防止第三方库持有对你函数的引用。

相关推荐
小政爱学习!16 分钟前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript
魏大帅。22 分钟前
Axios 的 responseType 属性详解及 Blob 与 ArrayBuffer 解析
前端·javascript·ajax
花花鱼28 分钟前
vue3 基于element-plus进行的一个可拖动改变导航与内容区域大小的简单方法
前端·javascript·elementui
k093332 分钟前
sourceTree回滚版本到某次提交
开发语言·前端·javascript
EricWang13581 小时前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端
September_ning1 小时前
React.lazy() 懒加载
前端·react.js·前端框架
web行路人1 小时前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
番茄小酱0011 小时前
Expo|ReactNative 中实现扫描二维码功能
javascript·react native·react.js
子非鱼9211 小时前
【Ajax】跨域
javascript·ajax·cors·jsonp
超雄代码狂1 小时前
ajax关于axios库的运用小案例
前端·javascript·ajax