元编程 (Meta Programming) 是指编写可以操作或改变其他程序的程序。元编程可以改变 JavaScript 的一些基本操作的行为

主要与这三个对象有关:

  • Symbol:通过内置 Symbol 值复写 JS 语言中的基本操作
  • Proxy:通过钩子函数拦截、改变 JS 语言的基本操作
  • Reflect:获取 JS 语言内部的基本操作

什么是基本操作

基本操作包括:属性访问和赋值、属性删除、属性枚举、函数调用、对象构造等

属性访问 (get) 和赋值 (set)

使用 . 或 [] 访问对象的属性,使用 = 给属性赋值

let obj = { a: 1 }
console.log(obj.a)    // 属性访问
obj.b = 2             // 属性赋值
console.log(obj['b']) // 属性访问

属性删除

let obj = { a: 1 }
delete obj.a

属性枚举

const obj = {
    name: 'test',
    age: 18,
}
 
for (k in obj) {
    console.log(obj[k])
}

函数调用

function add(a, b) {
    return a + b
}
 
const result = add(1, 2)

对象构造

function Person(name, age) {
    this.name = name
    this.age = age
}
 
const alice = new Person('Alice', 25)

Symbol

通过内置 Symbol 值复写 JS 语言中的内部实现

Reflect

Reflect 是一个内置的全局对象,对一些函数式的操作提供了面向对象的 API。 Reflect 不是一个函数对象,因此它是不可构造的。它所有的方法都是静态的,类似于 Math 对象

目前共 13 个静态方法,可以分为函数相关、原型相关、对象相关三大类

函数相关

apply

通过指定的 this 和参数列表发起对目标 (target) 函数的调用

Reflect.apply(target, thisArg, argumentsList)
  • 参数:
    • target:目标函数
    • thisArgtarget 函数调用时绑定的 this 对象
    • argumentsList:类数组,target 函数调用时的参数列表
  • 返回值:调用完带着指定参数和 this 值的给定的函数后返回的结果
  • 异常:如果 target 对象不可调用,抛出 TypeError 异常

示例:

function greet(name, age) {
  console.log(`Hello, my name is ${name} and I am ${age} years old.`);
}
const args = ['John', 30];
Reflect.apply(greet, null, args);
 
// 类似于:
// greet(...args)
// greet.apply(null, args)
// Function.prototype.apply.call(greet, null, args)

construct

该方法的行为有点像 [[007.原型与原型链#^new|new 操作符]] 构造函数,相当于运行 new target(...args)

Reflect.construct(target, argumentsList[, newTarget])
  • 参数:
    • target:被运行的目标构造函数
    • argumentsList:类数组,target 构造函数调用时的参数
    • newTarget(可选):作为新创建对象的原型对象的 constructor 属性,参考 new.target 操作符,默认值为 target
  • 返回值:以 target(如果 newTarget 存在,则为 newTarget)函数为构造函数,argumentList 为其初始化参数的对象实例
  • 异常:如果 target 或者 newTarget 不是构造函数,抛出 TypeError 异常

示例:

function Person(name, age) {
  this.name = name;
  this.age = age;
}
const args = ['John', 30];
const instance = Reflect.construct(Person, args);
 
// 类似于:
// new Person(...args)

Reflect.construct() Vs. Object.create()

function OneClass() {
  this.name = "one";
}
 
function OtherClass() {
  this.name = "other";
}
 
// 创建一个对象:
var obj1 = Reflect.construct(OneClass, args, OtherClass);
 
// 与上述方法等效:
var obj2 = Object.create(OtherClass.prototype);
OneClass.apply(obj2, args);
 
console.log(obj1.name); // 'one'
console.log(obj2.name); // 'one'
 
console.log(obj1 instanceof OneClass); // false
console.log(obj2 instanceof OneClass); // false
 
console.log(obj1 instanceof OtherClass); // true
console.log(obj2 instanceof OtherClass); // true

虽然两种方式结果相同,但在创建对象过程中仍一点不同:

  • 当使用 Object.create()Function.prototype.apply() 时,如果不使用 new 操作符调用构造函数,构造函数内部的 new.target 值会指向 undefined
  • 当调用 Reflect.construct() 来创建对象,new.target 值会自动指定到 target(或者 newTarget,前提是 newTarget 指定了)
function OneClass() {
  console.log("OneClass");
  console.log(new.target);
}
function OtherClass() {
  console.log("OtherClass");
  console.log(new.target);
}
 
var obj1 = Reflect.construct(OneClass, args);
// 输出:
//     OneClass
//     function OneClass { ... }
 
var obj2 = Reflect.construct(OneClass, args, OtherClass);
// 输出:
//     OneClass
//     function OtherClass { ... }
 
var obj3 = Object.create(OtherClass.prototype);
OneClass.apply(obj3, args);
// 输出:
//     OneClass
//     undefined

原型相关

getPrototypeOf

与 Object.getPrototypeOf() 方法几乎是一样的,都是返回指定对象的原型(即内部的 [[Prototype]] 属性的值)

Reflect.getPrototypeOf(target)
  • 参数:
    • target:获取原型的目标对象
  • 返回值:给定对象的原型。如果给定对象没有原型,则返回 null
  • 异常:如果 target 不是 Object,抛出一个 TypeError 异常

示例:

const obj = { x: 1 };
const proto = Reflect.getPrototypeOf(obj); // 相当于 Object.getPrototypeOf(obj)
console.log(proto === Object.prototype); // true

setPrototypeOf

除了返回类型以外,Reflect.setPrototypeOf() 与 Object.setPrototypeOf() 方法是一样的。它可设置对象的原型(即内部的 [[Prototype]] 属性)为另一个对象或 null,如果操作成功返回 true,否则返回 false

Reflect.setPrototypeOf(target, prototype)
  • 参数:
    • target:设置原型的目标对象
    • prototypetarget 对象的新原型(一个对象或 null
  • 返回值:返回一个 Boolean 值表明是否原型已经成功设置
  • 异常:如果 target 不是对象,或 prototype 既不是对象也不是 null,抛出一个 TypeError 异常

示例:

const obj = { x: 1 };
Reflect.setPrototypeOf(obj, Array.prototype); // 设置 obj 的原型为 Array.prototype
console.log(obj instanceof Array); // true

对象相关

get

用于获取属性值,与从对象中读取属性类似 (target[propertyKey]),但 receiver 参数可以改变 getterthis 对象

Reflect.get(target, propertyKey[, receiver])
  • 参数:
    • target:需要取值的目标对象
    • propertyKey:需要获取的值的键值
    • receiver(可选):receivergetter 调用时的 this
  • 返回值:属性的值
  • 异常:如果 target 不是 Object,抛出一个 TypeError 异常

set

用于设置属性值,等同于 target[propertyKey] = value,但 receiver 参数可以改变 setterthis 对象

Reflect.set(target, propertyKey, value[, receiver])
  • 参数:
    • target:设置属性的目标对象
    • propertyKey:需要设置的值的键值
    • value:设置的值
    • receiver(可选):receiversetter 调用时的 this
  • 返回值:一个 Boolean 值表明是否成功设置属性
  • 异常:如果 target 不是 Object,抛出一个 TypeError 异常

has

基本等同于 propertyKey in target,用于检查一个属性是否在某个对象中

Reflect.has(target, propertyKey)
  • 参数:
    • target:目标对象
    • propertyKey:属性名,需要检查目标对象是否存在此属性
  • 返回值:一个 Boolean 值指示是否存在此属性
  • 异常:如果 target 不是 Object,抛出一个 TypeError 异常

deleteProperty

用于删除属性,类似于 delete 操作符

Reflect.deleteProperty(target, propertyKey)
  • 参数:
    • target:目标对象
    • propertyKey:属性名
  • 返回值:一个 Boolean 值表明该属性是否被成功删除
  • 异常:如果 target 不是 Object,抛出一个 TypeError 异常

示例:

var obj = { x: 1, y: 2 };
Reflect.deleteProperty(obj, "x"); // true
obj; // { y: 2 }
 
var arr = [1, 2, 3, 4, 5];
Reflect.deleteProperty(arr, "3"); // true
arr; // [1, 2, 3, , 5]
 
// 如果属性不存在,返回 true
Reflect.deleteProperty({}, "foo"); // true
 
// 如果属性不可配置,返回 false
Reflect.deleteProperty(Object.freeze({ foo: 1 }), "foo"); // false

defineProperty

基本等同于 Object.defineProperty(),但返回值略有不同:

  • Object.defineProperty 返回一个对象,或者如果属性没有被成功定义,抛出一个 TypeError
  • Reflect.defineProperty 方法只返回一个 Boolean,来说明该属性是否被成功定义
Reflect.defineProperty(target, propertyKey, attributes)
  • 参数:
    • target:目标对象
    • propertyKey:要定义或修改的属性的名称
    • attributes:要定义或修改的属性的描述
  • 返回值:Boolean 值指示了属性是否被成功定义
  • 异常:如果 target 不是 Object,抛出一个 TypeError 异常

getOwnPropertyDescriptor

用于获取对象自身的某个属性的属性描述符,与 Object.getOwnPropertyDescriptor() 方法相似。如果在对象中存在,则返回给定的属性的属性描述符,否则返回 undefined

Reflect.getOwnPropertyDescriptor(target, propertyKey)
  • 参数:
    • target:需要寻找属性的目标对象
    • propertyKey:获取属性描述符的属性名
  • 返回值:如果属性存在于给定的目标对象中,则返回属性描述符;否则,返回 undefined
  • 异常:如果 target 不是 Object,抛出一个 TypeError 异常

ownKeys

返回一个由目标对象自身的属性键组成的数组,等同于 Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))

Reflect.ownKeys(target)
  • 参数:
    • target:目标对象
  • 返回值:由目标对象的自身属性键组成的 Array
  • 异常:如果 target 不是 Object,抛出一个 TypeError 异常

preventExtensions

使一个对象变为不可扩展(阻止新属性添加到对象),基本等同于 Object.preventExtensions(),但有一些不同:

  • Reflect.preventExtensions() 的第一个参数不是一个对象,将抛出一个 TypeError 异常
  • Object.preventExtensions() 非对象的第一个参数会被强制转换为一个对象
Reflect.preventExtensions(target)
  • 参数:
    • target:目标对象
  • 返回值:一个 Boolean 值表明目标对象是否成功被设置为不可扩展
  • 异常:如果 target 不是 Object,抛出一个 TypeError 异常

isExtensible

判断一个对象是否可扩展(即是否能够添加新的属性)。与 Object.isExtensible() 方法相似,但有一些不同:

  • Reflect.isExtensible() 的第一个参数不是一个对象,将抛出一个 TypeError 异常
  • Object.isExtensible() 非对象的第一个参数会被强制转换为一个对象
Reflect.isExtensible(target)
  • 参数:
    • target:目标对象
  • 返回值:返回一个 Boolean 值表明该对象是否可扩展
  • 异常:如果 target 不是 Object,抛出一个 TypeError 异常

Proxy

为什么 Proxy 里一定要使用 Reflect?

不使用 Reflect:

const obj = {
    _name: 'test',
    get name() {
        return this._name;
    }
};
const proxyObj = new Proxy(obj, {
    get(target, property, receiver) {
        return target[property];
    },
});
 
const child = {
    _name: 'child'
};
Reflect.setPrototypeOf(child, proxyObj);
 
child.name // test,期望为 child
proxyObj.name // test

因为代理对象的 get 拦截中固定返回的 target[property]target 永远指向 obj,所以拿到的永远是 obj_name 属性值

const obj = {
    _name: 'test',
    get name(){
        return this._name;
    }
};
const proxyObj = new Proxy(obj,{
    get(target, property, receiver){
        return Reflect.get(target, property, receiver)
    },
});
 
const child = {
    _name: 'child'
};
Reflect.setPrototypeOf(child, proxyObj);
 
child.name // child 
proxyObj.name // test

当使用 Reflect 时可以正确转发运行时上下文,其实主要就是 receiver 这个参数,receiver 代表的是代理对象本身或者继承自代理对象的对象,它表示触发 get 时正确的上下文