ES6

318 阅读14分钟

1. 字符串的拓展

1. 字符串遍历器接口

ES6位字符串添加了遍历接口,使得字符串可以被for...of循环遍历。

for (let codePoint of 'foo') {
     console.log(codePoint)
}
// "f"
// "o"
// "o"

2. 模板字符串

1. 标签模板

当模板字符串里有变量,就不是简单的调用,而是将模板字符串先处理成多个参数,再调用函数。

let a = 5;
let b = 10;

function tag(s, v1, v2) {
  console.log(s[0]);
  console.log(s[1]);
  console.log(s[2]);
  console.log(v1);
  console.log(v2);

  return "OK";
}

tag`Hello ${ a + b } world ${ a * b}`; //会转为tag(['Hello ', ' world ', ''], 15, 50)
// "Hello "
// " world "
// ""
// 15
// 50
// "OK"

再看一个更加复杂的例子,我们如何将各个参数按照原来的位置拼合回去

let total = 30;
let msg = passthru`The total is ${total} (${total*1.05} with tax)`;
//他会先处理成literals = ['The total is',30,'31.5 with tax']
function passthru(literals) {
  let result = '';
  let i = 0;

  while (i < literals.length) {
    result += literals[i++];
    if (i < arguments.length) {
      result += arguments[i];
    }
  }

  return result;
}

msg // "The total is 30 (31.5 with tax)"

既然他会把变量部分放在最后面,那么我们就可以采用rest参数的写法如下

function passthru(literals, ...values) {
  let output = "";
  let index;
  for (index = 0; index < values.length; index++) {
    output += literals[index] + values[index];
  }

  output += literals[index]
  return output;
}

3. 字符串的新增方法

1. String.raw()

该方法返回一个斜杠都被转义(即前面再加一个斜杠)的字符串。往往处理模板字符串

String.raw`Hi\n${2+3}!`
// 实际返回 "Hi\\n5!",显示的是转义后的结果 "Hi\n5!"

String.raw()本质上是一个正常的函数,只是专用于上述的模板字符串的标签函数。如果正常去写的话,第一个参数则是一个具有raw属性的对象,raw的值则是一个模板字符串转化出来值得数组。如下

// `foo${1 + 2}bar`
// 等同于
String.raw({ raw: ['foo', 'bar'] }, 1 + 2) // "foo3bar"

他的实现逻辑和上述的passthru一致,这里不再赘述。

2. 实例方法

1. includes(),startsWith(),endsWith()

在ES6之前,Js只有indexOf方法,来确定字符串是否包含。ES6中又提供了三种新方法。

  • includes() :返回布尔值,表示是否找到了参数字符串。
  • startsWith() :返回布尔值,表示参数字符串是否在原字符串的头部。
  • endsWith() :返回布尔值,表示参数字符串是否在原字符串的尾部。
let s = 'Hello world!';

s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true

上述三种方法都支持第二个参数,即开始搜索的位置。

2. repeat()

repeat方法返回一个新字符串,表示将原字符串重复n次

'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"

如果参数是小数,则会取证。参数是Infinity或负数,则会报错。0~-1取0,。NAN取0,字符串先转为数字。

3. padStart(),penEnd()

ES7中引入了上述两种不全字符串长度的两种方法,一共接受两个参数,第一个为补全最大长度,第二个为补全的字符串。

'x'.padStart(4, 'ab') // 'abax'

'x'.padEnd(5, 'ab') // 'xabab'

pdaStart()的常见用途比如为数字补全指定位数。

'123456'.padStart(10, '0') // "0000123456"

另一个用途则是提示字符串格式。

'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
'09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"

4. trimStart(),trimEnd()

ES9对字符串实例新增了上述两种方法。即消除头部、尾部空格

5. replaceAll()

在历史上,字符串的实例方法replace()只能替换第一个匹配。

'aabbcc'.replace('b', '_')
// 'aa_bcc'

在ES2021中,引入了上述方法,可以一次性替换所有的匹配。

'aabbcc'.replaceAll('b', '_')
// 'aa__cc'

他们都是返回一个新的字符串,不会改变原字符串。

6. at()

at方法返回参数指定位置的字符,支持负索引。

const str = 'hello';
str.at(1) // "e"
str.at(-1) // "o"

2. 函数的拓展

1. 函数参数的默认值

function Point(x = 0, y = 0) {
  this.x = x;
  this.y = y;
}

const p = new Point();
p // { x: 0, y: 0 }

1. 与解构赋值默认值结合使用

function foo({x, y = 5}) {
  console.log(x, y);
}

foo({}) // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo() // TypeError: Cannot read property 'x' of undefined

上述代码中,使用了对象的解构赋值,但是如果参数不穿对象时,则会报错,所以可以用默认值给到一个空对象。例如

function foo({x, y = 5} = {}) {
  console.log(x, y);
}

2. 参数带有默认值的位置

通常情况下,定义了默认值的参数,应该是函数的尾函数。如果不是尾函数设置默认值,则这个参数不能省略。

function f(x = 1, y) {
  return [x, y];
}
f(, 1) // 报错

3.函数的length属性

在制定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说length属性表明预期传入参数的个数,给了默认值就不算在预期中了,所以rest参数也不会计入。

4. 带有默认值的作用域

一旦参数设置了默认值,那么函数在进行声明初始化时,参数会形成一个单独的作用域。

let x = 1;

function f(y = x) {
  let x = 2;
  console.log(y);
}

f() // 1

上述代码中,给了参数y一个初始值为x,形成了一个作用域,在作用域中x没有定义,则指向外层的全局变量x,如果全局中也没有x,则会报错。

2. rest参数

ES6中引入了rest参数,这样就不需要使用arguments对象了,来看一下他们的区别

// arguments变量的写法
function sortNumbers() {
  return Array.from(arguments).sort();
}

// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();

因为arguments对象不是数组,而是一个类似数组的对象,需要用Array.from来转为数组。

注意rest参数之后就不能再有其他参数了。

3. name属性

返回函数的函数名。在ES5中,将一个匿名函数赋值给一个变量,会返回空。ES6中则会返回变量名。

var f = function () {};

// ES5
f.name // ""

// ES6
f.name // "f"

Function构造函数返回的函数实例,name属性的值为anonymous

(new Function).name // "anonymous"

bind返回的函数,name属性值会加上bound前缀。

function foo() {};
foo.bind({}).name // "bound foo"

(function(){}).bind({}).name // "bound "

4. 箭头函数

箭头函数可以与变量结构结合使用

const full = ({ first, last }) => first + ' ' + last;

// 等同于
function full(person) {
  return person.first + ' ' + person.last;
}

箭头函数也可以与rest参数结合使用

const numbers = (...nums) => nums;

numbers(1, 2, 3, 4, 5)
// [1,2,3,4,5]

箭头函数的注意事项

(1)箭头函数没有自己的this对象(详见下文)。

(2)不可以当作构造函数,也就是说,不可以对箭头函数使用new命令,否则会抛出一个错误。

(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。 首先第一点,箭头函数没有this,他的this始终指向函数定义时(上下文)所在的作用域。而普通函数的this则指向运行时所在的作用域,观察下述代码。

function Timer() {
  this.s1 = 0;
  this.s2 = 0;
  // 箭头函数
  setInterval(() => this.s1++, 1000);
  // 普通函数
  setInterval(function () {
    this.s2++;
  }, 1000);
}

var timer = new Timer();

setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0

前者的this绑定在Timer函数,后者的this则绑定在运行时的全局作用域中。 不适合使用箭头函数的情况

比如在对象中的方法中,就不适合使用箭头函数带this,因为对象没有单独的作用域,this为全局作用域。

globalThis.s = 21;

const obj = {
  s: 42,
  m: () => console.log(this.s)
};

obj.m() // 21

5. 尾调用优化

什么是尾调用呢???

尾调用就是在函数的最后一步,调用了另一个函数。比如

function f(x){
  return g(x);
}

如果在调用g(x)后,还有赋值操作,则不属于尾调用。 那怎么进行尾调用优化呢?

尾调用优化即在最后调用了g函数,那么f中的mn变量则不需要了,这样就可以完全删除f(x)的调用帧,而只保留g(3)的调用帧。- 只有safari支持。。。

function f() {
  let m = 1;
  let n = 2;
  return g(m + n);
}
f();

// 等同于
function f() {
  return g(3);
}
f();

// 等同于
g(3);

6. Function.prototype.toString()

ES2019对函数实例的toString()方法做了修改。会返回代码本身,包括注释和空格,原封不动!

function /* foo comment */ foo () {}

foo.toString()
// "function /* foo comment */ foo () {}"

3. 数组的拓展

1. 扩展运算符

它好比res参数的逆运算,将一个数组转为用逗号分隔的参数序列

console.log(...[1, 2, 3])
// 1 2 3

1. 替代apply方法

之前如果我们想将数组转为函数的参数,需要借助apply方法,如下。

function f(x, y, z) {
  // ...
}
var args = [0, 1, 2];
f.apply(null, args);

// ES6 的写法
function f(x, y, z) {
  // ...
}
let args = [0, 1, 2];
f(...args);

使用push时,因为push的参数不能是数组,所以也可以利用扩展运算符。

// ES5 的写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);

// ES6 的写法
let arr1 = [0, 1, 2];
let arr2 = [3, 4, 5];
arr1.push(...arr2);

2. 扩展运算符的应用

1. 复制数组

数组如果直接复制的话,只是复制了底层数据结构的指针,而不是克隆一个全新的数组。

const a1 = [1, 2];
const a2 = a1;

a2[0] = 2;
a1 // [2, 2]

所以上述代码,在改变a2时,其实是在改变指针,所以a1也会发生变化。在ES5中,我们可以使用concat()来复制数组

const a1 = [1, 2];
const a2 = a1.concat();

a2[0] = 2;
a1 // [1, 2]

而如果使用扩展运算符,则十分的简单。

const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;

2. 合并数组

const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];

// ES5 的合并数组
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]

// ES6 的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]

上述合并数组的方式都是通过对原数组的引用,都是浅拷贝。

3. 与解构赋值结合

const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest  // [2, 3, 4, 5]

const [first, ...rest] = [];
first // undefined
rest  // []

const [first, ...rest] = ["foo"];
first  // "foo"
rest   // []

4. 字符串

扩展运算符可以将字符串转成真数组

[...'hello']
// [ "h", "e", "l", "l", "o" ]

5. 实现了Iterator接口的对象

任何定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组。

let nodeList = document.querySelectorAll('div');
let array = [...nodeList];

2. Array.from()

Array.from()方法用于将类数组、可遍历的对象转为真数组。 在实际应用中,DOM操作返回的NodeList集合,以及函数内部的arguments对象,都可以通过Array.from()来转换。

// NodeList 对象
let ps = document.querySelectorAll('p');
Array.from(ps).filter(p => {
  return p.textContent.length > 100;
});

// arguments 对象
function foo() {
  var args = Array.from(arguments);
  // ...
}

Array.from()还可以接受一个函数作为第二个参数,作用类似于数组的map()方法.

Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);

3.Array.of()

Array.of()方法将一组值转换为数组。 Array.of(3, 11, 8) // [3,11,8]

4.实例方法

1. copyWithin()

将指定位置的成员复制到其他位置,会修改当前数组。将3号和4号位(第三号位元素)之间的数值覆盖到0号位。

// 将3号位复制到0号位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4)
// [4, 2, 3, 4, 5]

2. find()和findIndex()

find()找到第一个符合条件的数组成员,没有符合条件的成员则返回undefined。

[1, 4, -5, 10].find((n) => n < 0)
// -5

findIndex()则是返回第一个符合条件的数组成员的位置,没有则返回-1 上述两种方法都可以接受第二个参数,来绑定回调函数的this对象。

function f(v){
  return v > this.age;
}
let person = {name: 'John', age: 20};
[10, 12, 26, 15].find(f, person);    // 26

3. fill()

fill方法使用定值填充数组

['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']

4. entries(),keys().values()

ES6 提供三个新的方法——entries()keys()values()——用于遍历数组。 如果不使用for...of循环,可以手动调用遍历器对象的next方法,进行遍历。

let letter = ['a', 'b', 'c'];
let entries = letter.entries();
console.log(entries.next().value); // [0, 'a']
console.log(entries.next().value); // [1, 'b']
console.log(entries.next().value); // [2, 'c']

5. includes()

查看数组中是否存在给定的值,与字符串中的includes()方法类似。 对比indexOf(),它内部使用===来进行判断,这回导致对NaN进行误判。

6. flat(),flatMap()

将多层嵌套数组拉平,接收参数为拉平几层,拉平到底参数为Infinity

[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]

flatMap()则是先对数组进行map()再进行flat(),只能展开一层数组。

// 相当于 [[[2]], [[4]], [[6]], [[8]]].flat()
[1, 2, 3, 4].flatMap(x => [[x * 2]])
// [[2], [4], [6], [8]]

4. 对象的新增方法

1. Object.is()

在ES5中比较两值是否相等,只有== ===他们都有缺点。在ES6中引入Object.is()来比较两个值是否严格相等。

Object.is('foo', 'foo')  // true
Object.is({}, {})  // false
Object.is(+0, -0)  // false
Object.is(NaN, NaN) // true

2. Object.assign()

用于对象的合并,将源对象的所有可枚举的属性,复制到给定对象中。如果有同名属性,则会覆盖。

const target = { a: 1, b: 1 };

const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

如果参数不是对象则先转为对象,如果是undefinednull则会报错

注意

  • 浅拷贝

Object.assign()方法实行的是浅拷贝。

  • 数组的处理

Object.assign()方法可以用来处理数组,但是会把数组视为对象。

Object.assign([1, 2, 3], [4, 5])
// [4, 5, 3]
  • 取值函数的处理 如果复制的值是一个取值函数,那么先求值,再复制。
const source = {
  get foo() { return 1 }
};
const target = {};

Object.assign(target, source)
// { foo: 1 }

3. Object.getOwnPropertyDescriptors() 

再ES7中,引入该方法,返回的是自身属性的描述对象。该方法引入的目的是为了解决Object.assign()无法正确拷贝到getset属性的问题,它只会拷贝一个属性的值,而不会拷贝他背后的getset

我们可以通过Object.getOwnPropertyDescriptors()方法配合Object.defineProperties()方法,就可以实现正确拷贝。

const source = {
  set foo(value) {
    console.log(value);
  }
};

const target2 = {};
//将source的属性值给到target2
Object.defineProperties(target2,Object.getOwnPropertyDescriptors(source);
Object.getOwnPropertyDescriptor(target2, 'foo')
// { get: undefined,
//   set: [Function: set foo],
//   enumerable: true,
//   configurable: true }

4. __proto__属性,Object.setPrototypeOf(),Object.getPrototypeOf()

__proto__属性他是一个内部属性,我们可以用下面的Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。

5.Symbol

1. 概述

Symbol可以生成一个独一无二的值,它可以接收一个字符串作为参数,主要是为了在控制台显示,或者转成字符串便于区分。注意即使参数相同的Symbol函数的返回值也是不相等的。

let s1 = Symbol('foo');
let s2 = Symbol('bar');

s1 // Symbol(foo)
s2 // Symbol(bar)

s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"

2. Symbol.prototype.description

我们在读取Symbol显式的字符串时,只能toString(),再ES2019中,增加了description来直接返回描述。

3. 作为属性名的Symbol

既然每一个Symbol都不相等,这样我们就可以使用Symbol的值作为标识符。

let mySymbol = Symbol();

// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';

// 第二种写法
let a = {
  [mySymbol]: 'Hello!'
};

// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上写法都得到同样结果
a[mySymbol] // "Hello!"

注意Symbol值作为对象属性名时,不能用点运算符,在对象内部使用时,也必须放在方括号之中。

4. 属性名遍历

Symbol作为属性名,在遍历对象时,不会被返回,可以通过Object.getOwnPropertySymbols()方法,获取所有的Symbol属性名,返回一个数组。

另一个新的 API,Reflect.ownKeys()方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。

5. Symbol.for(),Symbol.keyFor()

当我们想要多次使用同一个Symbol值时,可以使用Symbol.for(),他会在全局注册,当你下次使用时,会在全局中搜索,如果存在就返回之前注册的值。

Symbol.keyFor()则是返回一个已经登记的Symbol的key,使用Symbol注册的是没有登记的哦。

6. Set和Map数据结构

1. Set

1. 基本用法

Set类似于数组,但是成员的值都是唯一的。他本身是一个构造函数,用来生成Set数据结构。

既然他成员的值唯一,那么他就可以去除数组中重复的字符。在Set中,两个NaN是相等的,两个空对象是不相等的。

// 去除数组的重复成员
[...new Set(array)]

2. Set的实例属性&方法

Set 结构的实例有以下属性。

  • Set.prototype.constructor:构造函数,默认就是Set函数。
  • Set.prototype.size:返回Set实例的成员总数。

Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。

  • Set.prototype.add(value):添加某个值,返回 Set 结构本身。
  • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。
  • Set.prototype.clear():清除所有成员,没有返回值。

Array.from方法可以将 Set 结构转为数组。

const items = new Set([1, 2, 3, 4, 5]);
const array = Array.from(items);

3.遍历操作

  • Set.prototype.keys():返回键名的遍历器
  • Set.prototype.values():返回键值的遍历器
  • Set.prototype.entries():返回键值对的遍历器
  • Set.prototype.forEach():使用回调函数遍历每个成员
for (let item of set.entries()) {
  console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]

注意Set结构的键名和键值是一个值。

使用Set和扩展运算符可以很容易的实现并集、交集、差集

let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);

// 并集
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}

// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}

// (a 相对于 b 的)差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}

2. WeakSet

他跟Set类似,但是有两个区别。

第一:WeakSet的成员只能是对象,如果我们将a做为WeakSet得参数,那意味着a的成员必须是对象才可以。如果a = [1,2,3,4],就会报错。

const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}

3. Map

1.基本用法

在Js中的对象只能用字符串当做键,为了解决这个问题,ES6提供了Map数据结构,但是键的范围不限于字符串。

2. 实例的属性和方法

1. size

size属性返回Map结构的成员总数

2. Map.prototype.set(key,value),Map.prototype.get(key),Map.prototype.has(key),Map.prototype.delete(key),Map.prototype.clear()

设置键值对,返回整个Map结构;读取key对应的键值;是否存在当前Map之中;删除key,返回是否删除成功;清空所有成员,没有返回值。

3.遍历相关内容同Set基本一直

4. WeakMap

WeakMap只接收对象为键名。他主要是为了解决如下问题:

当我们在对象上存放某些数据时,可能会引用其他元素,这样当我们不需要该对象时,如果我们不手动删除这个引用,那么垃圾回收机制将不会释放。

const e1 = document.getElementById('foo');
const e2 = document.getElementById('bar');
const arr = [
  [e1, 'foo 元素'],
  [e2, 'bar 元素'],
];

而WeakMap里面的键名引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内,也就是说在其他引用清除后,WeakMap中的引用会自动消失。

最典型的应用场合就是DOM节点作为键名。一旦这个DOM节点删除,该状态就会自动消失。

let myWeakmap = new WeakMap();

myWeakmap.set(
  document.getElementById('logo'),
  {timesClicked: 0})
;

document.getElementById('logo').addEventListener('click', function() {
  let logoData = myWeakmap.get(document.getElementById('logo'));
  logoData.timesClicked++;
}, false);

7. Proxy

1. Proxy概述

Proxy可以理解成在目标对象之前,设置一层拦截(代理)

var obj = new Proxy({}, {
  get: function (target, propKey, receiver) {
    console.log(`getting ${propKey}!`); //getting count!
    return Reflect.get(target, propKey, receiver);
  },
  set: function (target, propKey, value, receiver) {
    console.log(`setting ${propKey}!`);
    return Reflect.set(target, propKey, value, receiver);
  }
});

ES6提供了Proxy构造函数,用来生成Proxy实例。

var proxy = new Proxy(target,handler); //target表示拦截的目标对象; handler用来定制拦截行为。
var proxy = new Proxy({} , {
    get(target,propKey){
        return 35;
    }
});
proxy.time // 35
proxy.name // 35

Proxy 实例也可以作为其他对象的原型对象。

var proxy = new Proxy({}, {
  get: function(target, propKey) {
    return 35;
  }
});

let obj = Object.create(proxy);
obj.time // 35

2. proxy实例方法

1. get()

var a = {name:'张三'}
a.name // 张三
a.age // undefined
//设置get拦截器,target表示拦截对象,propKey为属性名,receiver为实例本身
var proxy = new Proxy(a,{
  get(target,propKey,receiver){
    if(propKey in target){
      return target[propKey]
    } else {
      throw new Error(propKey + 'does not exist')
    }
  }
})
proxy.getReceiver === proxy // true
proxy.age // 抛出错误

get 方法可以继承

let obj = Object.create(proxy)
obj.age // 抛出错误

如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,否则通过 Proxy 对象访问该属性会报错。

const target = Object.defineProperties({}, {
  foo: {
    value: 123,
    writable: false,
    configurable: false
  },
});

const handler = {
  get(target, propKey) {
    return 'abc';
  }
};

const proxy = new Proxy(target, handler);

proxy.foo
// TypeError: Invariant check failed

2. set()

set()和get()类似,参数多了一个修改的value,下面例子为对象中以_开头的对象为不可使用。

const handler = {
  get (target, key) {
    invariant(key, 'get');
    return target[key];
  },
  set (target, key, value) {
    invariant(key, 'set');
    target[key] = value;
    return true;
  }
};
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Invalid attempt to ${action} private "${key}" property`);
  }
}
const target = {};
const proxy = new Proxy(target, handler);
proxy._prop
// Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c'
// Error: Invalid attempt to set private "_prop" property

3. this问题

虽然Proxy可以代理目标对象,但是目标对象的内部this关键字会指向Proxy代理。

const target = {
  m: function () {
    console.log(this === proxy);
  }
};
const handler = {};

const proxy = new Proxy(target, handler);

target.m() // false
proxy.m()  // true

拦截函数handlerthis指向的是handler对象

const handler = {
  get: function (target, key, receiver) {
    console.log(this === handler);
    return 'Hello, ' + key;
  },
  set: function (target, key, value) {
    console.log(this === handler);
    target[key] = value;
    return true;
  }
};

const proxy = new Proxy({}, handler);

proxy.foo
// true
// Hello, foo

proxy.foo = 1
// true