阅读视图

发现新文章,点击刷新页面。

JavaScript Proxy 和 Reflect

一、Proxy 基本概念

1. 什么是 Proxy

Proxy 是 ES6 引入的元编程特性,用于创建一个对象的代理,从而可以拦截和自定义对象的基本操作。

const target = {};
const handler = {
  get(target, property) {
    return `拦截读取: ${property}`;
  }
};
const proxy = new Proxy(target, handler);

console.log(proxy.message); // "拦截读取: message"

2. 核心术语

  • target:被代理的目标对象
  • handler:包含拦截器(trap)的对象
  • trap:拦截目标对象操作的函数

二、Proxy 拦截操作

1. 常用拦截器

属性访问拦截

const handler = {
  get(target, prop) {
    console.log(`读取属性 ${prop}`);
    return target[prop];
  },
  set(target, prop, value) {
    console.log(`设置属性 ${prop} = ${value}`);
    target[prop] = value;
    return true; // 表示设置成功
  }
};

方法调用拦截

const handler = {
  apply(target, thisArg, argumentsList) {
    console.log(`调用函数,参数: ${argumentsList}`);
    return target.apply(thisArg, argumentsList);
  }
};

function sum(a, b) { return a + b; }
const proxySum = new Proxy(sum, handler);
proxySum(1, 2); // 输出日志后返回3

2. 完整拦截器列表

拦截器 触发时机 示例
get 读取属性 proxy.x
set 设置属性 proxy.x = 1
has in 操作符 'x' in proxy
deleteProperty delete 操作 delete proxy.x
apply 函数调用 proxy()
construct new 操作 new proxy()
ownKeys Object.keys()等 Object.keys(proxy)
getPrototypeOf Object.getPrototypeOf() Object.getPrototypeOf(proxy)
setPrototypeOf Object.setPrototypeOf() Object.setPrototypeOf(proxy, proto)
isExtensible Object.isExtensible() Object.isExtensible(proxy)
preventExtensions Object.preventExtensions() Object.preventExtensions(proxy)
getOwnPropertyDescriptor Object.getOwnPropertyDescriptor() Object.getOwnPropertyDescriptor(proxy, 'x')
defineProperty Object.defineProperty() Object.defineProperty(proxy, 'x', desc)

三、Proxy 高级应用

1. 数据验证

const validator = {
  set(target, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value) || value < 0) {
        throw new TypeError('年龄必须是正整数');
      }
    }
    target[prop] = value;
    return true;
  }
};

const person = new Proxy({}, validator);
person.age = 30; // 正常
person.age = 'young'; // 抛出错误

2. 自动填充对象

const autoFiller = {
  get(target, prop) {
    if (!(prop in target)) {
      target[prop] = {};
    }
    return target[prop];
  }
};

const tree = new Proxy({}, autoFiller);
tree.branch1.branch2.leaf = 'value';
console.log(tree); // { branch1: { branch2: { leaf: 'value' } } }

3. 负数组索引

const negativeArray = arr => new Proxy(arr, {
  get(target, prop, receiver) {
    const index = parseInt(prop);
    if (index < 0) {
      prop = target.length + index;
    }
    return Reflect.get(target, prop, receiver);
  }
});

const arr = negativeArray(['a', 'b', 'c']);
console.log(arr[-1]); // "c"

四、Reflect 基本概念

1. 什么是 Reflect

Reflect 是一个内置对象,提供拦截 JavaScript 操作的方法,这些方法与 Proxy 的拦截器一一对应。

const obj = { x: 1 };
console.log(Reflect.get(obj, 'x')); // 1

2. Reflect 的设计目的

  1. 统一操作 API:将语言内部操作(如 [[Get]][[Set]])暴露为函数
  2. 与 Proxy 配合:每个 Proxy 拦截器都有对应的 Reflect 方法
  3. 替代部分 Object 方法:更合理的返回值设计

五、Reflect 主要方法

1. 基本操作方法

方法 等价操作 区别
Reflect.get(target, prop) target[prop] 支持 receiver
Reflect.set(target, prop, value) target[prop] = value 返回布尔值
Reflect.has(target, prop) prop in target -
Reflect.deleteProperty(target, prop) delete target[prop] 返回布尔值
Reflect.ownKeys(target) Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target)) -

2. 对象扩展方法

方法 等价操作 区别
Reflect.defineProperty(target, prop, desc) Object.defineProperty(target, prop, desc) 返回布尔值
Reflect.getOwnPropertyDescriptor(target, prop) Object.getOwnPropertyDescriptor(target, prop) -
Reflect.isExtensible(target) Object.isExtensible(target) -
Reflect.preventExtensions(target) Object.preventExtensions(target) 返回布尔值

3. 原型相关方法

const obj = {};
const proto = { x: 1 };

// 设置原型
Reflect.setPrototypeOf(obj, proto);
console.log(obj.x); // 1

// 获取原型
console.log(Reflect.getPrototypeOf(obj) === proto); // true

4. 函数调用方法

function greet(name) {
  return `Hello, ${name}`;
}

// 函数调用
console.log(Reflect.apply(greet, null, ['Alice'])); // "Hello, Alice"

// 构造函数调用
class Person {}
const p = Reflect.construct(Person, []);
console.log(p instanceof Person); // true

六、Proxy 和 Reflect 配合使用

1. 默认转发模式

const proxy = new Proxy(target, {
  get(target, prop, receiver) {
    console.log(`GET ${prop}`);
    return Reflect.get(target, prop, receiver);
  },
  set(target, prop, value, receiver) {
    console.log(`SET ${prop}=${value}`);
    return Reflect.set(target, prop, value, receiver);
  }
});

2. 实现观察者模式

function createObservable(target, observer) {
  return new Proxy(target, {
    set(target, prop, value, receiver) {
      const oldValue = target[prop];
      const result = Reflect.set(target, prop, value, receiver);
      if (result && oldValue !== value) {
        observer(prop, oldValue, value);
      }
      return result;
    }
  });
}

const data = createObservable({}, (prop, oldVal, newVal) => {
  console.log(`属性 ${prop}${oldVal} 变为 ${newVal}`);
});

data.x = 1; // 输出: "属性 x 从 undefined 变为 1"
data.x = 2; // 输出: "属性 x 从 1 变为 2"

七、实际应用场景

1. API 客户端封装

function createAPI(baseUrl) {
  return new Proxy({}, {
    get(target, endpoint) {
      return async function(params) {
        const url = `${baseUrl}/${endpoint}`;
        const response = await fetch(url, {
          method: 'POST',
          body: JSON.stringify(params)
        });
        return response.json();
      };
    }
  });
}

const api = createAPI('https://api.example.com');
const userData = await api.users({ id: 123 });

2. 不可变数据

function createImmutable(obj) {
  return new Proxy(obj, {
    set() {
      throw new Error('不可修改');
    },
    deleteProperty() {
      throw new Error('不可删除');
    },
    defineProperty() {
      throw new Error('不可定义新属性');
    }
  });
}

const immutable = createImmutable({ x: 1 });
immutable.x = 2; // 抛出错误

3. 自动缓存

function createCached(fn) {
  const cache = new Map();
  return new Proxy(fn, {
    apply(target, thisArg, args) {
      const key = JSON.stringify(args);
      if (cache.has(key)) {
        return cache.get(key);
      }
      const result = Reflect.apply(target, thisArg, args);
      cache.set(key, result);
      return result;
    }
  });
}

const heavyCompute = createCached(x => {
  console.log('计算中...');
  return x * x;
});

heavyCompute(2); // 输出"计算中..."后返回4
heavyCompute(2); // 直接返回4,不输出

八、注意事项与最佳实践

1. Proxy 限制

  1. 目标对象不变:Proxy 不会修改目标对象本身
  2. 严格相等proxy === target 为 false
  3. 内部插槽:某些内置对象(如 Date、Map)的内部插槽无法被拦截

2. 性能考虑

  1. 代理有开销:简单操作用原生方式更快
  2. 避免多层代理:代理的代理会显著降低性能
  3. 热路径优化:关键性能路径避免使用代理

3. 最佳实践

  1. 透明代理:尽量保持代理行为与目标对象一致
  2. 合理使用 Reflect:在拦截器中使用对应 Reflect 方法
  3. 明确文档:记录代理的自定义行为
  4. 错误处理:在拦截器中妥善处理异常

九、浏览器兼容性

1. 支持情况

  • Proxy:主流浏览器及 Node.js 6+
  • Reflect:主流浏览器及 Node.js 6+

2. 不支持的解决方案

// 简单的Proxy polyfill检查
if (typeof Proxy === 'undefined') {
  console.warn('Proxy not supported, falling back to direct access');
  Proxy = function(target) { return target; };
}

十、总结

Proxy 和 Reflect 为 JavaScript 提供了强大的元编程能力:

  1. Proxy

    • 创建对象的代理,拦截基本操作
    • 实现数据绑定、验证、观察等高级功能
    • 自定义对象基本行为
  2. Reflect

    • 提供操作对象的统一API
    • 与Proxy拦截器一一对应
    • 改进Object方法的返回值设计

两者结合使用可以实现许多传统JavaScript难以实现的高级模式,如:

  • 透明的数据观察
  • 自动化的资源管理
  • 动态接口适配
  • 领域特定语言(DSL)

这些特性在现代框架和库中有广泛应用,如Vue 3的响应式系统就基于Proxy实现。合理使用Proxy和Reflect可以显著提升代码的灵活性和可维护性。

ES6~ES13 新特性

一、ES6 (ES2015) 重要新特性

1. 模板字符串 (Template Literals)

  • 使用反引号(``)定义字符串
  • 支持多行字符串和插值表达式
// 基本用法
const name = 'Alice';
const greeting = `Hello, ${name}!`; // "Hello, Alice!"

// 多行字符串
const multiLine = `
  This is 
  a multi-line
  string
`;

// 标签模板
function tag(strings, ...values) {
  console.log(strings); // ["Hello ", "!"]
  console.log(values);  // ["Alice"]
  return strings[0] + values[0].toUpperCase() + strings[1];
}
const result = tag`Hello ${name}!`; // "Hello ALICE!"

2. 箭头函数 (Arrow Functions)

  • 更简洁的函数语法
  • 自动绑定词法作用域的 this
// 基本语法
const add = (a, b) => a + b;

// 单参数可省略括号
const square = x => x * x;

// 无参数需要括号
const sayHi = () => console.log('Hi');

// 返回对象需要用括号包裹
const makePerson = (name, age) => ({ name, age });

// this 绑定示例
const counter = {
  count: 0,
  increment: function() {
    setInterval(() => {
      this.count++; // 正确绑定this
      console.log(this.count);
    }, 1000);
  }
};

3. 函数参数增强

默认参数 (Default Parameters)

function greet(name = 'Guest', greeting = 'Hello') {
  return `${greeting}, ${name}!`;
}
console.log(greet()); // "Hello, Guest!"

剩余参数 (Rest Parameters)

function sum(...numbers) {
  return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3)); // 6

4. 解构赋值 (Destructuring Assignment)

对象解构

const person = { name: 'John', age: 30, city: 'New York' };
const { name, age } = person;
console.log(name, age); // "John" 30

// 别名
const { name: personName } = person;
console.log(personName); // "John"

// 默认值
const { country = 'USA' } = person;
console.log(country); // "USA"

数组解构

const numbers = [1, 2, 3];
const [first, second] = numbers;
console.log(first, second); // 1 2

// 跳过元素
const [,, third] = numbers;
console.log(third); // 3

// 剩余元素
const [head, ...tail] = numbers;
console.log(tail); // [2, 3]

5. 对象字面量增强

属性简写

const name = 'Alice';
const age = 25;
const person = { name, age }; // { name: 'Alice', age: 25 }

方法简写

const obj = {
  // 传统写法
  sayHello: function() {},
  
  // 简写写法
  sayHi() {},
  
  // 箭头函数
  sayBye: () => {}
};

计算属性名

const propKey = 'name';
const obj = {
  [propKey]: 'John',
  [`get${propKey}`]() {
    return this[propKey];
  }
};
console.log(obj.getName()); // "John"

二、ES2016 (ES7) 新特性

1. 数组 includes() 方法

const arr = [1, 2, 3];
console.log(arr.includes(2)); // true
console.log(arr.includes(4)); // false

// 与indexOf的区别
console.log([NaN].includes(NaN)); // true
console.log([NaN].indexOf(NaN) !== -1); // false

2. 指数运算符 (**)

console.log(2 ** 3); // 8
console.log(Math.pow(2, 3)); // 8

// 与Math.pow()的区别
console.log((-2) ** 3); // -8
console.log(Math.pow(-2, 3)); // -8

三、ES2017 (ES8) 新特性

1. async/await

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Error:', error);
  }
}

2. Object.values() / Object.entries()

const obj = { a: 1, b: 2, c: 3 };

console.log(Object.values(obj)); // [1, 2, 3]
console.log(Object.entries(obj)); // [["a", 1], ["b", 2], ["c", 3]]

// 与for...in的区别
for (const [key, value] of Object.entries(obj)) {
  console.log(`${key}: ${value}`);
}

3. 字符串填充方法

console.log('5'.padStart(2, '0')); // "05"
console.log('12'.padStart(2, '0')); // "12"
console.log('abc'.padEnd(5, '*')); // "abc**"

4. 函数参数列表尾逗号

function foo(
  param1,
  param2, // 允许尾逗号
) {
  // ...
}

四、ES2018 (ES9) 新特性

1. 对象扩展运算符

const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 }; // { a: 1, b: 2, c: 3 }

// 覆盖属性
const obj3 = { ...obj1, a: 3 }; // { a: 3, b: 2 }

// 浅拷贝
const obj4 = { ...obj1 };
console.log(obj4 === obj1); // false

2. Promise.finally()

fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error))
  .finally(() => console.log('Request completed'));

3. 正则表达式增强

命名捕获组

const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = re.exec('2023-05-15');
console.log(match.groups.year); // "2023"

dotAll 模式 (s 标志)

const re = /foo.bar/s;
console.log(re.test('foo\nbar')); // true

后行断言

console.log(/(?<=\$)\d+/.exec('$100')[0]); // "100" (匹配$后的数字)
console.log(/(?<!\$)\d+/.exec('€100')[0]); // "100" (匹配前面不是$的数字)

五、ES2019 (ES10) 新特性

1. Array.flat() / Array.flatMap()

const arr = [1, [2, [3]]];
console.log(arr.flat()); // [1, 2, [3]]
console.log(arr.flat(2)); // [1, 2, 3]

const sentences = ["Hello world", "Goodbye universe"];
const words = sentences.flatMap(sentence => sentence.split(' '));
console.log(words); // ["Hello", "world", "Goodbye", "universe"]

2. Object.fromEntries()

const entries = [['name', 'John'], ['age', 30]];
const obj = Object.fromEntries(entries);
console.log(obj); // { name: "John", age: 30 }

// 与Object.entries()配合使用
const newObj = Object.fromEntries(
  Object.entries(obj).map(([key, value]) => [key, String(value)])
);

3. String.trimStart() / String.trimEnd()

const str = '   hello   ';
console.log(str.trimStart()); // "hello   "
console.log(str.trimEnd());   // "   hello"

4. 可选的 catch 绑定

try {
  // ...
} catch { // 不需要指定error参数
  console.log('An error occurred');
}

六、ES2020 (ES11) 新特性

1. 可选链操作符 (?.)

const user = { profile: { name: 'John' } };

// 传统写法
const name = user && user.profile && user.profile.name;

// 可选链写法
const name = user?.profile?.name;

// 函数调用
user.sayHi?.(); // 如果sayHi存在则调用

// 数组访问
const firstItem = arr?.[0];

2. 空值合并运算符 (??)

const value = null ?? 'default'; // "default"
const value2 = 0 ?? 'default';   // 0 (与||不同)

// 与||的区别
console.log(0 || 'default'); // "default"
console.log('' || 'default'); // "default"
console.log(false || 'default'); // "default"

3. 动态导入 (Dynamic Import)

// 按需加载模块
button.addEventListener('click', async () => {
  const module = await import('./module.js');
  module.doSomething();
});

4. BigInt

const bigNum = 9007199254740991n; // 字面量加n
const anotherBigNum = BigInt("9007199254740991");

console.log(bigNum + anotherBigNum); // 18014398509481982n

// 不能与Number混合运算
console.log(bigNum + 1); // TypeError

七、ES2021 (ES12) 新特性

1. 逻辑赋值运算符

// 逻辑或赋值
let a = false;
a ||= true; // a = a || true

// 逻辑与赋值
let b = true;
b &&= false; // b = b && false

// 空值合并赋值
let c = null;
c ??= 'default'; // c = c ?? 'default'

2. String.replaceAll()

const str = 'hello world';
console.log(str.replaceAll('l', 'x')); // "hexxo worxd"

// 之前需要正则表达式
console.log(str.replace(/l/g, 'x')); // "hexxo worxd"

3. Promise.any()

const promises = [
  Promise.reject('Error 1'),
  Promise.resolve('Success 1'),
  Promise.reject('Error 2')
];

Promise.any(promises)
  .then(result => console.log(result)) // "Success 1"
  .catch(errors => console.log(errors));

4. 数字分隔符

const billion = 1_000_000_000; // 更易读
console.log(billion === 1000000000); // true

八、ES2022 (ES13) 新特性

1. 类字段声明

class Counter {
  count = 0; // 实例字段
  
  increment = () => { // 箭头函数方法
    this.count++;
  };
  
  static version = '1.0'; // 静态字段
}

2. 私有字段和方法

class Person {
  #name; // 私有字段
  
  constructor(name) {
    this.#name = name;
  }
  
  #privateMethod() { // 私有方法
    return `Hello, ${this.#name}`;
  }
  
  greet() {
    console.log(this.#privateMethod());
  }
}

const person = new Person('John');
person.greet(); // "Hello, John"
// person.#name; // SyntaxError

3. 静态初始化块

class Translator {
  static translations = {
    yes: 'ja',
    no: 'nein'
  };
  
  static englishWords = [];
  static germanWords = [];
  
  static { // 静态初始化块
    for (const [english, german] of Object.entries(this.translations)) {
      this.englishWords.push(english);
      this.germanWords.push(german);
    }
  }
}

4. 顶层 await

// 在模块顶层使用await
const data = await fetchData();
console.log(data);

// 之前需要包装在async函数中
(async () => {
  const data = await fetchData();
  console.log(data);
})();

九、ES2023 (ES14) 新特性

1. Array.findLast() / Array.findLastIndex()

const numbers = [1, 2, 3, 4, 3, 2, 1];

console.log(numbers.findLast(n => n > 2)); // 3 (最后一个满足条件的元素)
console.log(numbers.findLastIndex(n => n > 2)); // 4 (索引)

2. Hashbang 语法标准化

#!/usr/bin/env node
// 现在ES标准正式支持Shebang语法
console.log('Hello from CLI tool');

3. WeakMap 支持 Symbol 键

const weakMap = new WeakMap();
const key = Symbol('privateData');
const obj = {};

weakMap.set(key, 'secret');
weakMap.set(obj, 'other secret');

console.log(weakMap.get(key)); // "secret"
console.log(weakMap.get(obj)); // "other secret"

十、总结与应用建议

1. 版本特性快速参考

版本 重要特性
ES6 类、模块、箭头函数、模板字符串、解构、Promise、let/const
ES2016 includes()、指数运算符
ES2017 async/await、Object.values/entries、字符串填充
ES2018 对象扩展运算符、Promise.finally、正则增强
ES2019 Array.flat/flatMap、Object.fromEntries、trimStart/End
ES2020 可选链、空值合并、动态导入、BigInt
ES2021 逻辑赋值、replaceAll、Promise.any、数字分隔符
ES2022 类字段、私有方法、静态块、顶层await
ES2023 Array.findLast、Hashbang、WeakMap Symbol键

2. 现代JavaScript开发建议

  1. 优先使用新语法:如箭头函数、模板字符串、解构赋值等
  2. 渐进式采用:根据项目环境逐步引入新特性
  3. 关注兼容性:使用Babel等工具确保代码兼容性
  4. 代码可读性:合理使用新特性提升代码可读性而非炫技
  5. 团队一致性:制定团队编码规范统一新特性的使用方式

3. 学习资源推荐

  1. 官方文档ECMAScript规范
  2. 兼容性查询Can I use
  3. 实践教程现代JavaScript教程
  4. 新特性演示ES6+示例
❌