JS反射和代理(Reflect 和 Proxy )

在了解 ReflectProxy 之前,需要知道 JavaScript 规范以及引擎带有一系列的内部方法,例如 [[Get]][[Set]][[Prototype]] 等。这类用双 [] 包裹的属性被称为"内部槽和内部方法"(Internal slots and methods)。它们不是对象的属性,不能作为 JavaScript 代码的属性直接访问。

什么是内部槽和内部方法?

内部槽 是 JavaScript 对象的数据成员或规范类型,用于存储对象的状态。例如,每个 JavaScript 对象都有一个 [[Prototype]] 内部槽,用于指向对象的原型。

内部方法 是 JavaScript 对象的成员函数。例如,每个 JavaScript 对象都有一个 [[GetOwnProperty]] 内部方法,由引擎在需要获取对象自身属性时调用。

这些内部槽和内部方法是 JavaScript 引擎内部实现的一部分,并不直接暴露给开发者。相反,它们通过规范来定义其行为和要求,从而保证不同引擎实现的一致性。

为什么内部槽和内部方法很重要?

内部槽和内部方法有助于确保 JavaScript 对象的一致行为。在不同的 JavaScript 引擎(如 V8、SpiderMonkey 等)中,尽管每个引擎可能有不同的实现方式,它们都必须按照同一套规范来实现这些内部槽和内部方法。

这使得 JavaScript 程序能够在不同的环境中表现出一致的行为,从而提高了代码的跨平台兼容性和可移植性。

常见的内部方法

内部方法 签名 描述
[[GetPrototypeOf]] () → Object / Null 获取对象的原型。返回提供继承属性的对象,若无,则返回null。对应方法:Object.getPrototypeOf()
[[SetPrototypeOf]] (Object / Null) → Boolean 设置对象的原型。将对象与另一个提供继承属性的对象关联。成功返回true,失败返回false。对应方法:Object.setPrototypeOf()
[[IsExtensible]] () → Boolean 判断对象是否可扩展。对应方法:Object.isExtensible()
[[PreventExtensions]] () → Boolean 防止对象扩展。控制新属性是否能被加入对象内。成功返回true,失败返回false。对应方法:Object.preventExtensions()
[[GetOwnProperty]] (propertyKey) → Undefined / Property Descriptor 获取对象自身属性的属性描述符。若无对应属性,返回undefined。对应方法:Object.getOwnPropertyDescriptor()Object.getOwnPropertyDescriptors()
[[DefineOwnProperty]] (propertyKey, PropertyDescriptor) → Boolean 定义或修改对象自己的属性。成功返回true,失败返回false。对应方法:Object.defineProperty()Object.defineProperties()
[[HasProperty]] (propertyKey) → Boolean 判断对象是否拥有自身或继承的某个属性。对应方法:Object.prototype.hasOwnProperty()
[[Get]] (propertyKey, Receiver) → any 获取对象中指定属性的值。如果需要执行ECMAScript代码来获得属性值,Receiver 被当作 this 使用。
[[Set]] (propertyKey, value, Receiver) → Boolean 设置对象中指定属性的值。Receiver 被当作 this 使用。成功返回true,失败返回false
[[Delete]] (propertyKey) → Boolean 删除对象中指定属性。成功返回true,失败返回false。类似于 JavaScript 中的 delete 关键字。
[[OwnPropertyKeys]] () → List of propertyKey 返回包含对象自身所有属性键的列表。对应方法:Object.getOwnPropertyNames()Object.getOwnPropertySymbols()

函数对象支持的其他基本内部方法

内部方法 签名 描述
[[Call]] (any, a List of any) → any 执行与对象关联的代码。通过函数调用表达式调用。第一个参数表示 this,第二个参数是传入的参数列表。实现此内部方法的对象是可调用的。
[[Construct]] (a List of any, Object) → Object 创建一个对象。通过 newsuper 操作调用。第一个参数是包含运算符参数的列表,第二个参数是初始应用对象。实现该内部方法的对象称为 constructors。非构造函数对象没有 [[Construct]] 内部方法。

2. Proxy 和 Reflect 的作用

JavaScript 提供了 ProxyReflect 对象来拦截和操作这些内部方法的行为。


description: Reflect反射

因为在早期的ECMA规范中没有考虑到这种对 对象本身 的操作如何设计会更加规范,所以将这些API放到了 Object 上面,Object 作为一个构造函数,这些操作实际上放到它身上并不合适,还包含一些类似于 in、delete操作符,让JS看起来是会有一些奇怪的

在ES6中新增了 Reflect ,让我们这些操作都集中到了Reflect对象上,它提供了一些反射方法,这些方法与那些在 ObjectFunction 原型上的方法具有相同的名称和功能。Reflect 的引入主要是为了使操作对象的行为变得更规范和一致,并且提供一个与 Proxy 对象互补的 API。

Object.getOwnPropertyNames(Reflect) 获取到'Reflect' 上的属性,因为这些属性都是不可枚举的因此没有使用'Object.keys' 来获取,一共获取了13个静态方法

['defineProperty', 'deleteProperty', 'apply', 'construct', 'get', 'getOwnPropertyDescriptor', 'getPrototypeOf', 'has', 'isExtensible', 'ownKeys', 'preventExtensions', 'set', 'setPrototypeOf']

Reflect 13个静态方法大致可以分为三类:对象操作、函数调用和原型操作

对象操作方法

  • Reflect.get(target, propertyKey[, receiver]) :获取对象的属性值,相当于 target[propertyKey]
  • Reflect.set(target, propertyKey, value[, receiver]) :设置对象的属性值,相当于 target[propertyKey] = value
  • Reflect.deleteProperty(target, propertyKey) :删除对象的属性值,相当于 delete target[propertyKey]
  • Reflect.has(target, propertyKey) :检查对象是否有某个属性,相当于 propertyKey in target
  • Reflect.defineProperty(target, propertyKey, descriptor) :定义对象的属性,相当于 Object.defineProperty(target, propertyKey, descriptor)
  • Reflect.getOwnPropertyDescriptor(target, propertyKey) :获取对象自有属性的描述符,相当于 Object.getOwnPropertyDescriptor(target, propertyKey)
  • Reflect.ownKeys(target) :返回对象的所有自有属性的键,相当于 Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))

函数调用方法

  • Reflect.apply(target, thisArgument, argumentsList) :调用一个函数,相当于 Function.prototype.apply.call(target, thisArgument, argumentsList)
  • Reflect.construct(target, argumentsList[, newTarget]) :构造一个实例,相当于 new target(...argumentsList)

原型操作方法

  • Reflect.getPrototypeOf(target) :获取对象的原型,相当于 Object.getPrototypeOf(target)
  • Reflect.setPrototypeOf(target, prototype) :设置对象的原型,相当于 Object.setPrototypeOf(target, prototype)
  • Reflect.isExtensible(target) :检查对象是否是可扩展的,相当于 Object.isExtensible(target)
  • Reflect.preventExtensions(target) :让一个对象变得不可扩展,相当于 Object.preventExtensions(target)

使用案例

Reflect.get(target, name, receiver)

好的,我来优化这段文字,使其更容易理解。


  1. Reflect.get(target, propertyKey, [receiver]) 方法的作用类似于从对象中读取属性(如 target[propertyKey]),但它是通过一个函数来实现的。如果 propertyKey 对应的属性是一个 getter 函数,receiver 参数将被作为 this 使用。如果没有提供 receiver 参数,那么 this 将指向 target。需要注意的是,只有在属性是 getter 函数时receiver 才能改变 this 的指向。

  2. 调用 Reflect.get() 类似于使用 target[propertyKey] 表达式,因为它也会查找目标对象原型链上的属性值。如果目标对象上不存在该属性,则返回 undefined

js 复制代码
const a = {
  name: "w",
  age: "18",

  get info() {
    return this.name + this.age;
  },
};

console.log(Reflect.get(a, "name")); // w
console.log(Reflect.get(a, "info")); // w18

// 别的对象借用获取属性
console.log(Reflect.get(a, "info", { name: "z", age: "20" })); // z20

Reflect.set(target, propertyKey, value[, receiver])

等同于 target[propertyKey] = value

js 复制代码
// 设置对象的属性值
Reflect.set(obj, 'b', 2);
console.log(obj.b); // 2

Reflect.deleteProperty(target, propertyKey)

对应的内部插槽 [[Delete]] 方法等同于delete obj[name],用于删除对象的属性,方法的第一个参数不是对象,会报错 Reflect.deleteProperty(target, propertyKey)

js 复制代码
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

Reflect.has

Reflect.has方法检查target对象或其原型上的propertyKey属性是否存在。这与in操作符完全相同。如果找到属性,则返回true,否则返回false,对应内部插槽 [[HasProperty]]

js 复制代码
'assign' in Object // true

// 新写法
Reflect.has(Object, 'assign') // true

Reflect.ownKeys(target)

Reflect.ownKeys 使用的内部插槽 [[OwnPropertyKeys]] ,方法返回一个由目标对象自身的属性键组成的数组。它的返回值等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))。结合了 Object.getOwnPropertyNamesObject.getOwnPropertySymbols 的结果,返回对象自身的所有属性键,包括字符串键和符号键。即自身所有属性(包含可枚举不可枚举和 symbol 属性)

  • Object.keys(obj):返回一个数组,包含对象自身的所有可枚举属性键,但不包括符号键。
  • Object.getOwnPropertyNames(obj):返回一个数组,包含对象自身的所有字符串属性键,但不包括符号键。
  • Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身的所有符号属性键。
js 复制代码
const obj = {
  property1: 42,
  [Symbol("property2")]: "symbol value",
  say() {},
};

// 增加不可枚举
Object.defineProperty(obj, "hidden", {
  value: "not enumerable",
  enumerable: false,
});

// 获取目标对象的所有属性键(包括符号键)
const keys = Reflect.ownKeys(obj);
console.log(keys); // [ 'property1', 'say', 'hidden', Symbol(property2) ]

// 只能可枚举的
console.log(Object.keys(obj)); // [ 'property1', 'say' ]

// 不能获取symbol的
console.log(Object.getOwnPropertyNames(obj)); // [ 'property1', 'say', 'hidden' ]
// 只能获取symbol的
console.log(Object.getOwnPropertySymbols(obj)); // [ Symbol(property2) ]
  • 注意包含对象自身的所有符号属性键 用继承来说明
js 复制代码
class Base {
  constructor() {
    this.baseProperty = "base";
  }

  baseMethod() {}
}

class Derived extends Base {
  constructor() {
    super();
    this.derivedProperty = "derived";
  }
  [Symbol("property2")] = "symbol value";

  derivedMethod() {}
}

const derivedObj = new Derived();

const keys = Reflect.ownKeys(derivedObj);

// 制造 可枚举的比较复杂 这里数据没有用可枚举

console.log(keys); // [ 'baseProperty', 'derivedProperty', Symbol(property2) ]

console.log(Object.keys(derivedObj)); // [ 'baseProperty', 'derivedProperty' ] // 只能可枚举的属性

console.log(Object.getOwnPropertyNames(derivedObj)); // [ 'baseProperty', 'derivedProperty' ] // 不能获取symbol属性

console.log(Object.getOwnPropertySymbols(derivedObj)); // [ Symbol(property2) ]

如上图如果你想获取继承的属性 console.log(Reflect.ownKeys(Reflect.getPrototypeOf(derivedObj))); 不停从原型链获取即可

Reflect.defineProperty(target, propertyKey, attributes)

Reflect.defineProperty(target, propertyKey, attributes) 使用的内部插槽[[DefineOwnProperty]]

Object.definePropertyReflect.defineProperty 两者在功能上非常相似,但它们在返回值和异常处理上有所不同。我们来详细对比一下:

  • Object.defineProperty 如果操作成功,返回目标对象 obj。如果操作失败,抛出一个 TypeError 异常。

  • Reflect.defineProperty如果操作成功,返回 true。 如果操作失败,返回 false

javascript 复制代码
// 使用 Object.defineProperty
try {
  Object.defineProperty({}, 'key', { value: 42, writable: false });
  console.log('属性定义成功');
} catch (e) {
  console.error('Object.defineProperty 失败:', e);
}

// 使用 Reflect.defineProperty
const success = Reflect.defineProperty({}, 'key', { value: 42, writable: false });
if (success) {
  console.log('属性定义成功');
} else {
  console.log('Reflect.defineProperty 失败');
}

Reflect.getOwnPropertyDescriptor(target, propertyKey)

返回目标对象的非继承属性的属性描述符,即非 [[Prototype]](__proto__) 上的属性描述符。和 Object.getOwnPropertyDescriptor 有略微的不同

  • Object.getOwnPropertyDescriptor(target, propertyKey) 对非对象的 target 参数会被强制转换为对象。

  • Reflect.getOwnPropertyDescriptor target 必须是一个对象。如果传入非对象类型,会抛出异常(TypeError)。

    javascript 复制代码
    const obj = { key: 42 };
    const descriptor = Reflect.getOwnPropertyDescriptor(obj, 'key');
    console.log(descriptor);
    // 输出: { value: 42, writable: true, enumerable: true, configurable: true }
    
    // 非对象的情况会抛出 TypeError
    try {
      Reflect.getOwnPropertyDescriptor(42, 'key');
    } catch (e) {
      console.error(e); // TypeError: Reflect.getOwnPropertyDescriptor called on non-object
    }
    javascript 复制代码
    const obj = { key: 42 };
    const descriptor = Object.getOwnPropertyDescriptor(obj, 'key');
    console.log(descriptor);
    // 输出: { value: 42, writable: true, enumerable: true, configurable: true }
    
    // 基本类型会被强制转换为对象
    const strDescriptor = Object.getOwnPropertyDescriptor('foo', 'length');
    console.log(strDescriptor);
    // 输出: { value: 3, writable: false, enumerable: false, configurable: false }
js 复制代码
Reflect.getOwnPropertyDescriptor({x: "hello"}, "x");
// {value: "hello", writable: true, enumerable: true, configurable: true}

Reflect.getOwnPropertyDescriptor({x: "hello"}, "y");
// undefined

Reflect.getOwnPropertyDescriptor([], "length");
// {value: 0, writable: true, enumerable: false, configurable: false}



// ------------------------------ 继承属性 ---------------------------------------
const sy = Symbol('a')

class A {
    name = 123;
    [sy] = 145
    getName() {}
}

class B extends A {
    getAge() {}
}
const b = new B()
console.log(Reflect.getOwnPropertyDescriptor(b, 'getName'))
console.log(Reflect.getOwnPropertyDescriptor(b, 'getAge'))
console.log(Reflect.getOwnPropertyDescriptor(b, 'name'))

// // 打印结果:
// undefined
// undefined
// { value: 123, writable: true, enumerable: true, configurable: true }

Reflect.getPrototypeOf(target)

1.静态方法 Reflect.getPrototypeOf(target) Object.getPrototypeOf() 方法几乎是一样的。都是返回指定对象的原型(即内部的 [[Prototype]] 属性的值)。如果目标没有原型,则返回null。 Reflect 要求target 必须是对象,Object 则不用 会自动进行拆箱转换

js 复制代码
Reflect.getPrototypeOf( null) // TypeError: Reflect.getPrototypeOf called on non-object

Reflect.getPrototypeOf( 'hello') // TypeError: Reflect.getPrototypeOf called on non-object

Reflect.getPrototypeOf( {})  // Object {constructor: Object(), __defineGetter__: ƒ, ...} 

Reflect.setPrototypeOf(target, prototype)

Reflect.setPrototypeOf(target, prototype),返回一个 Boolean 值表明是否原型已经成功设置,内部的 [[Prototype]] 属性值对应 Object.setPrototypeOf()

js 复制代码
const object1 = {};

console.log(Reflect.setPrototypeOf(object1, Object.prototype));
// expected output: true

console.log(Reflect.setPrototypeOf(object1, null));
// expected output: true

const object2 = {};

console.log(Reflect.setPrototypeOf(Object.freeze(object2), null));
// expected output: false

Reflect.preventExtensions(target)

在实际开发中,Reflect.preventExtensions 可以用来保护对象,使其不能再添加新的属性。虽然现有属性仍然可以修改或删除,但它提供了一种基本的对象保护机制,帮助你更好地控制对象的结构和类型。

  • Reflect.preventExtensions 返回操作成功的布尔值。
  • Object.preventExtensions 返回修改后的对象。
  • 如果参数不是对象,Reflect.preventExtensions 会抛出异常,而 Object.preventExtensions 会进行类型转换。
js 复制代码
let obj = {name: "Alice"};

// 默认对象是可扩展的
console.log(Reflect.isExtensible(obj)); // 输出: true

// 使用 Reflect.preventExtensions 使对象不可扩展
Reflect.preventExtensions(obj);
console.log(Reflect.isExtensible(obj)); // 输出: false

// 尝试添加新属性会失败
obj.age = 30;
console.log(obj.age); // 输出: undefined (属性添加失败)

// 现有属性仍然可以修改
obj.name = "Bob";
console.log(obj.name); // 输出: "Bob"

// 现有属性可以删除
delete obj.name;
console.log(obj.name); // 输出: undefined

Reflect.isExtensible(target)

Reflect.isExtensible(target) target 必须是一个对象如果是非对象会异常报错,对应的内部插槽 [[IsExtensible]] ,检查对象是否可以进行扩展,返回一个 Boolean 值表明目标对象是否可扩展对应的方法,Object.isExtensible() 方法, 不同点是它对非对象的 target 参数将被强制转换为对象。

对象的"可扩展性"指的是是否可以向对象添加新的属性。默认情况下,所有对象都是可扩展的。但是,使用某些方法(如 Object.preventExtensionsObject.sealObject.freeze)可以防止对象的扩展。

  • Object.preventExtensions(obj): 使对象不可扩展。
  • Object.seal(obj): 使对象不可扩展且所有现有属性不可配置。
  • Object.freeze(obj): 使对象不可扩展、所有现有属性不可配置且不可写。
js 复制代码
let obj = {};

// 默认对象是可扩展的
console.log(Reflect.isExtensible(obj)); // 输出: true

// 停止对象的扩展
Object.preventExtensions(obj);
console.log(Reflect.isExtensible(obj)); // 输出: false

// 使用 Object.freeze 冻结对象也会使其不可扩展
let frozenObj = Object.freeze({});
console.log(Reflect.isExtensible(frozenObj)); // 输出: false

// Object.seal 同样会使对象不可扩展
let sealedObj = Object.seal({});
console.log(Reflect.isExtensible(sealedObj)); // 输出: false

Reflect.apply(target, thisArgument, argumentsList)

1. Reflect.apply 的作用: Reflect.apply 对应 JavaScript 内部的 [[call]] 插槽,用于调用一个函数。其作用等同于 ES5 时期的 Function.prototype.apply.call,特别是在自定义函数的 apply 方法被重写但仍希望调用其原生 apply 方法时,Reflect.apply 更为简洁和直观。

2. Function.prototype.apply.call 的解析:

  • apply 方法 :接受两个参数,第一个是 thisArg(函数运行时的 this 值),第二个是 传给函数参数的数组
  • call 方法 :接受多个参数,第一个参数是 thisArg,之后是函数的参数分别列出。

例如:

javascript 复制代码
Function.prototype.apply.call(someFunc, thisArg, [argsArray]);

等同于:

javascript 复制代码
someFunc.apply(thisArg, argsArray);

3. Reflect.apply 的简化: 使用 Reflect.apply(target, thisArg, args)

  • target:目标函数
  • thisArg:函数调用时的 this
  • args:调用时的参数数组

4. 参数处理差异:

  • Function.prototype.apply 中,第二个参数可以为 nullundefined,函数仍会正常执行。
  • Reflect.apply 中,如果参数数组为 nullundefined 会抛出 TypeError 异常。

示例:

javascript 复制代码
getName.apply(null, null); // 正常执行

Reflect.apply(getName, null, null); // TypeError 异常
Reflect.apply(getName, null, ""); // TypeError 异常
Reflect.apply(getName, null); // TypeError 异常

// 如果不传参数,应该这样写
Reflect.apply(getName, null, {}); // 或
Reflect.apply(getName, null, []); // 正常执行



const obj = { f: 'w' }

var f = 'windows'
function getName(age) {
    console.log(age, this.f)
}


getName(12)
getName.apply(obj, [12])
Reflect.apply(getName, null, [12])
Reflect.apply(getName, obj, [12])
Function.prototype.apply.call(getName, obj, [12])
// 打印结果
12 'windows'
12 'w'
12 'windows'
12 'w'
12 'w'

Reflect.construct(target, argumentsList, [, newTarget])

Reflect.construct(target, argumentsList[, newTarget]) 用于创建一个新的实例对象,并且该对象的原型是由 newTarget 确定的。

  • 代表目标构造函数的第一个参数。

  • 第二个参数,代表目标构造函数的参数,以类数组格式传递。

  • 可选参数,指定新创建对象的原型对象的 constructor 属性。如果不提供,默认值为 target

Reflect.construct 映射到 JavaScript 内部的 [[Constructor]] 插槽,等同于以下代码:

javascript 复制代码
var obj = new Foo(...args);

Reflect.apply 一样,如果不需要对目标函数传参,那么需要传一个空数组,否则会报错。

javascript 复制代码
Reflect.construct(target, null); // TypeError 异常
Reflect.construct(target); // TypeError 异常
Reflect.construct(target, []); // 不传参的情况

使用第三个参数 newTarget 可以改变新创建对象的 [[Prototype]] 指向,从而改变 __proto__。这意味着新创建的对象构造函数可以指向 newTarget,如下所示:

javascript 复制代码
class OneClass {
    constructor() {
        this.name = 'OneClass';
    }
}

class OtherClass {
    constructor() {
        this.name = 'OtherClass';
    }
}

// 生成一个对象,其 `[[Prototype]]` 指向 `OtherClass.prototype`
let obj = Reflect.construct(OneClass, [], OtherClass);

console.log(obj instanceof OtherClass); // true
console.log(obj instanceof OneClass); // false
console.log(obj.name); // "OneClass"

这等同于如下代码:

javascript 复制代码
// 创建一个以 `OtherClass.prototype` 为原型的对象
var obj2 = Object.create(OtherClass.prototype);
// 调用 `OneClass`,将 `this` 指向新对象
OneClass.apply(obj2, []);

注意:上述代码实际上调用的是 OneClass 的内部 [[Call]] 插槽,而不是 [[Constructor]]this 绑定到了新创建的对象上,因此 new.target 在这种情况下为 undefined,这和使用 Reflect.construct 是不同的。

js 复制代码
class A {
    name = 'a'
    age = 'b'
    getAge() {}
}

class B {
    zz = 12
    getName() {}
}

const b = Reflect.construct(A, {}, B)
console.log(b instanceof A) // false
console.log(b instanceof B) // true
相关推荐
hackeroink39 分钟前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者2 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-3 小时前
验证码机制
前端·后端
燃先生._.4 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖5 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235245 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240256 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar6 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人6 小时前
前端知识补充—CSS
前端·css