重学ES6 1.0


let 和 const

1
2
3
4
5
6
var a= [];
for (let i = 0; i < 10; i++) {
a [ i] = function () {
console.log(i);
a[6J ();// 6
//每轮的i都是重新声明的 因为js引擎内部会记住上一轮循环的值 另外 设置循环变量的那部分是一个父作用域 而循环体内部是一个单独的子作用域

暂时性死区的本质就是只要进入当前作用域 所要使用的变量就已经存在 但是不可获取 只有等到声明变量的那一行代码出现 才可以获取和使用变量。

块级作用域

ES6 允许块级作用域声明函数

  • 函数声明类似于var 即会提升到全局作用域或者函数作用域的头部
  • 函数声明还会提升到所在块级作用域的头部

do 表达式

1
2
3
4
5
let x = do {
let t = f();
t * t + 1;
};
//变量x会得到块级作用域的返回值

const

const实际上保证的并不是变量的值不得改动 而是变量指向的那个内存地址不得改动(对于一个复合类型的数据 变量指向的内存地址保存的只是一个指针 const 只能保证这个指针是固定的 不能保证它指向的数据结构是不可变的)

变量的结构赋值

数组的解构赋值

ES6 内部使用严格相等运算符判断一个位置是否有值 所以 如果一个数组成员不严格等于undefined 默认值是不会生效的

1
2
3
4
5
6
7
let [x=1] = [undefined]; // x = 1
let [x=1] = [null];// x = null
//如果默认值是一个表达式 那么这个表达式是惰性求值的
function f() {....};
let [x = f()] = [1];
//默认值可以引用解构赋值的其他变量 但是该变量必须已经声明
let [x = y, y = 1] = [];//ReferenceError

对象的解构赋值

数组的元素是按次序排列的 变量的取值是由他的位置决定的 而对象的属性没有次序 变量必须与属性同名才能取到正确的值 0

默认值生效的条件是对象的属性值严格等于undefined 如果解构失败 那么变量的值等于undefined

如果解构的是嵌套的对象 而且子对象所在的父属性不存在 那么就会报错

1
2
3
let {
foo: { bar },
} = { baz: "baz" }; //foo = undefined 所以取其子元素就会报错
1
2
let arr = [1, 2, 3];
let { 0: first, [arr.length - 1]: last } = arr; //first: 1, last: 3

数值和布尔值的解构赋值

解构赋值时 只要等号右边的值不是对象或数组 就会将其转为对象 由于undefinednull无法转为对象 所以对他们进行解构赋值时都会报错

解构的用途

  • 交换变量的值
1
2
3
let x = 1;
let y = 2;
[x, y] = [y, x];
  • 从函数返回多个值
  • 函数参数的命名
  • 提取JSON数据
  • 函数参数的默认值
  • 遍历Map结构
1
2
3
for (let [key] of map) {
....
}
  • 输入模块的指定方法

字符串的扩展

includes() startsWith() endsWith()

  • includes(): 返回布尔值 表示是否找到了字符串
  • startsWith(): 返回布尔值 表示参数字符串是否在源字符串的头部
  • endsWith(): 返回布尔值 表示参数字符串是否在源字符串的头部

repeat()

返回一个新字符串 将原来的字符串重复几次

padStart() padEnd()

1
2
3
4
5
6
'x'.padStart(5, 'ab')//'ababx' 最小长度以及补全的字符串
//如果字符串的长度大于或等于指定的最小长度 则返回原字符串 如果省略第二个参数 则用空格来补全
//padStart常用来胃数值补全指定位
1 ’ . padStart(l 0,’0 ’ ) // ” 0 00000000 1 ”
//也可以用来提示字符串格式
'12'.padStart(10, 'YYYY-MM-DD')//'YYYY-MM-12'

模板字符串

模板字符串默认会将其他字符串转移 导致无法嵌入其他语言

String.raw()

充当模板字符串的处理函数(返回一个连反斜线都被转转义的字符串)

1
2
String.raw`Hi\n${5+3}`;///  Hi \\nS !
String.raw Hi\\n ;// ” Hi \\n” 如果原字符串中的反斜线已经转移 则不会处理

正则的扩展

字符串的正则方法

image.png

数值的扩展

Number.isFinite():判断一个数是不是有限的

Number.isNaN():判断一个数是不是 NaN

只对数值有效 对非数值一律返回 false

Number.EPSILON

极小的常量 实质是一个可以接受的误差范围

可用于为浮点数设置一个误差范围

1
2
3
4
5
function withinErrorMargin(left, right) {
return Math.abs(left - right) < Number.EPSILON;
}
withinErrorMargin(0.1 + 0.2, 0.3); //true
withinErrorMargin(0.2 + 0.2, 0.3); //false

Math 对象的新增

Math.trunc()

用于取出一个数的小数部分 返回整数部分(先内部使用 Number 再转为数值 对于空值或者无法截取整数的值 返回 NaN)

1
Math.trunc = Math.trunc || (x) => x < 0? Math.ceil(x): Math.floor(x)

Math.sign()

判断一个数到底是正数负数还是零 先转为数值

函数的扩展

参数默认值

参数默认值不是传值的 而是每次都重新计算默认表达式的值 也就是默认表达式其实是惰性求值的

1
2
3
4
5
let x = 99;
const foo = (p = x + 1) => console.log(p);
foo(); //100
x = 100;
foo(); //101

函数的length返回没有设置默认值的参数个数

作用域

一旦设置了默认值 函数进行声明式初始化 参会会形成一个单独的作用域 等到初始化结束 这个作用域就会消失 这和不设置参数的默认行为不一样

1
2
3
4
5
6
7
8
9
let x = 1;
const f = (y = x) => {
let x = 2;
console.log(y);
};
f();//1 参数y = x形成一个单独的作用域 在这个作用域里面 x还未定义 所以指向外层的全局变量x 函数体内部的局部变量x影响不到默认值变量x 如果全局x不存在 就会报错
var x = 1;
const foo =(x = x) => {...};
foo();//ReferenceError : x is not defined x = x会形成一个单独的作用域 实际上就是let x = x 由于暂时性死区 执行这行代码会产生定义错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var x = 1;
function foo(x, y = function() {x = 2}) {//定义了x
var x = 3;
y();//改变的是参数定义的x
console.log(x) //输出var定义的x
}
foo();//3
console.log(x);//1
---------------------------
var x = 1;
function foo (x , y = function () { x = 2 ; } ) {
x = 3; //改变参数的x
y(); //改变定义的x
console.log(x);
}
foo () // 2
x // 1

箭头函数

  1. 函数体内的this就是定义时所在的对象 而不是使用时所在的对象
    1. this指向的固定化并不是因为箭头函数内部由绑定 this 的机制 而是因为箭头函数根本没有自己的this 导致内部的this就是外层代码块的this
  2. 不可以当作构造函数
    1. 因为它没有this 所以不能用作构造函数
  3. 不可以使用arguments对象 可以用rest参数代替
  4. 不可以使用yield命令 因此箭头函数不能也能做Generator函数

绑定 this

使用(::)会自动将左边的对象作为上下文环境(即this对象)绑定到右边的函数上

1
foo::bar; // bar.bind(foo);

可以链式调用

尾调用优化

函数调用会在内部形成一个调用记录调用帧)保存调用位置和内部变量等信息 所有调用帧就形成一个调用栈 尾调用由于是函数的最后一步操作 所以不需要保留外层函数的调用栈 因为调用位置 内部变量等信息都不会再用到 所以直接用内层函数的调用帧取代外层函数的即可。

1
2
3
4
5
6
7
8
9
10
11
12
function f() {
let m = 1;
let n = 2;
return g(m + n); //需要保存m n 和g的调用位置
}
f();
//等同于
function f() {
return g(3); //只保留g3的调用帧
}
f();
//等同于

尾调用优化即指保留内层函数的调用帧 如果函数都是尾调用 那么可以做到每次执行时调用帧只有一项 可以大大节省内存

只有不再用到外层函数的内部变量 内层函数的调用帧才可以取代外层函数的调用帧 否则无法进行尾调用优化

尾递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function factorial (n) {
if (n === 1) return 1;
return n * factorial(n - 1) ;
factorial(S) // 120 容易栈溢出 复杂度On
//用尾调用优化
function factorial (n , total) {
if (n === 1) return total ;
return factorial(n - 1 , n *total);
factorial(S, 1) // 120 复杂的O1
----------------
function Fibonacc 工( {
if ( n <= 1 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2) ;
Fibonacci (lO) // 89
Fibonacci(lOO) // 堆裁溢出
//尾调用优化
function Fibonacci2 (n , acl = 1 , ac2 = 1) {
if ( n <= 1 ) {return ac2} ;
return Fibonacci2 (n - 1 , ac2, acl + ac2);
Fibonacc i2 (100) // 573147844013817200000
Fibonacci2 (1 000) // 7. 0330367711422765e+208
Fibonacci2(10000) //Infinit

递归函数的改写

  • 在尾递归函数之外再提供一个正常形式的函数
1
2
3
4
5
6
7
function tailFactorial(n, total) {
......
}
function factorial(n) {
return tailFactorial(n, 1);
}
factorial(5);
  • 使用 ES6 的函数默认值
1
2
3
4
5
function factorial(n, total = 1) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5); //120

一旦使用递归 则最好使用尾递归

ES2017 提议可以在最后一个参数后面加逗号 减少后期更改时的提交信息的冗余

数组的扩展

解构赋值

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

如果将扩展运算符用于数组赋值 则只能将其作为参数的最后一位

任何Iterator接口的对象 都可以使用扩展运算符来转为真正的数组

1
2
var nodeList = document.querySelectorAll("div");
var array = [...nodeList];

Array.from()

将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)对象

Array.from的第二个参数类似于map方法 将每个元素处理后放入返回的数组

1
2
Array.from(new Set(arr)); //数组去重
Array.from(string).length; //将字符串转换为数组并返回字符串的长度 避免特殊字符算作两个字符的bug

Array.of()

将一组值转换为数组

copyWithin()

在当前数组内部将指定位置的成员复制到其他位置(会覆盖原有裁员) 然后返回数组 即这个方法会修改当前数组

Array.prototype.copyWithin(target, start = 0, end = this.length)

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

find()和 findIndex()

找到符合条件值以及对应的索引

fill()

使用一个定制填充一个数组

entries()、keys()、values()

includes()

  • Map结构的has方法是用来查找键名的
  • Set结构的has方法是用来查找值的

数组的空位

空位不是 undefined 一个位置的值等于 undefined 依然是有值的 空位是没有任何值的 in 运算符可以说明这点

ES6 规定将空位转为undefined

对象的扩展

Object.is()

与全等运算符差不多 但是NaN等于NaN +0 != -0

Object.assign()

将源对象的所有可枚举属性复制到目标对象

如果目标对象和源对象有同名属性或者多个源有同名属性 则后面的属性会覆盖前面的属性

如果只有一个参数 则Object.assign直接返回参数 如果该参数不是对象 则先转成对象再返回 如果是null或者 undefined 则报错

源对象(非首参数)位置的参数会先转成对象 不能则跳过 粗了字符串会以数组的形式复制到目标对象 其他值不会产生效果

Object.assign复制的属性是有限的 只复制源对象的自身属性(不复制继承属性) 也不复制不可枚举的属性

Object.assign是浅复制 不是深复制

对于嵌套的对象 一旦遇到同名属性 Object.assign的处理方法是替换而不是添加

1
2
3
var target = { a: { b: "c", d: "e" } };
var source = { a: { b: "hello" } };
Object.assign(target, source); //{a: {b: 'hello'}}

Object.assign 可以用来处理数组 但是会把数组当作对象来处理

用途

  • 为对象添加属性
1
2
3
4
5
class Point {
constructor(x, y) {
Object.assign(this, {x, y) );
}
}
  • 为对象添加方法
1
2
3
Object . assign (SomeClass . prototype , {
someMethod (argl , arg2) {
}
  • 克隆对象
1
2
3
4
5
6
7
8
function clone(origin) {
return Object.assign({} , origin);
}
//将原始对象赋值到一个新的对象 只能克隆自身的值 不能克隆继承的值
function clone(origin) {
let originProto = Object . getPrototypeOf( ori gin );
return Object.assign(Object . create(originProto) , origin );
}
  • 合并多个对象
1
2
3
4
5
6
7
const merge = (target, ...source) => Object.assign(target, ...source)
//如果希望合并后返回一个新对象 可以对一个空对象合并
const merge = (...source) => Object.assign({}, ...source);
const data1 = {name: 'james'};
const data2 = merge(data1);
data2.name = 'curry'
console.log(data1.name)://'james';//深复制
  • 为属性指定默认值

由于存在深复制的问题 DEFAULTS 对象和 options 对象的所有属性都只能是简单类型 而不能指向另一个对象 否则将导致 DEFAULTS 对象该属性不起作用

属性的遍历

  • for...in

    • 遍历对象自身和继承的可枚举属性(不含Symbol属性)
  • Object.keys(obj)

    • 返回一个包含自身(不包含Symbol 以及继承)的可枚举属性
  • Object.getOwnPropertyNames(obj)

    • 包含自身(不包含Symbol但是包含不可枚举属性)的数组
  • Object.getOwnPropertySymbols(obj)

    • 包含自身的所有Symbol属性
  • Reflect.ownKeys(obj)

    • 包含自身的所有属性

遍历规则

  • 首先遍历所有属性名为数值的属性 按照数字排序
  • 其次遍历所有属性名为字符串的属性 按照生成时间遍历
  • 最后遍历所有属性名为Symbol 的属性 按照生成时间排序

Object.setPrototypeOf()

设置一个对象的prototype对象 返回参数本身

Object.getPrototypeOf()

读取一个对象的prototype对象

如果参数不是对象 则自动转换为对象 如果是undefinednull 则直接报错

Objecr.keys() Object.values() Object.entries()

  • Object.keys():包含自身(不包含继承)的所有可遍历属性

  • Object.values():如果参数不是对象 则会先将其转为对象 对于数值或者布尔值则返回空数组

  • Object.entries():输出非Symbol值的属性

    • ‘可以将对象转为真正的Map结构
    1
    2
    3
    var obj = { foo: "bar", baz: 40 };
    var map = new Map(Object.entries(obj));
    map; //{foo: 'bar', baz: 40}

对象的扩展运算符

解构赋值的复制是浅复制 即如果一个键的值是复合类型的值(数组、对象、函数) 那么解构赋值复制的是这个值的引用 而不是这个值的副本

解构赋值不会复制继承自原型对象的属性

1
2
3
//克隆完整对象
const clone = Object.assign(Object.create(Object.getPrototypeOf(obj)), obj);
//修改现有对象

Object.assign总是复制一个属性的值 而不会复制它背后的赋值方法或取值方法

Object.getOwnPropertyDescriptions配合Object.defineProperties可以实现正确复制

1
2
3
4
5
6
7
8
const source = {
set foo(value) {
console.log(value);
},
};
const target2 = {};
Object.defineProperties(target2, Objecxt.getOwnPropertyDescriptions(source));
Object.getOwnPropertyDescriptiontor(target2, "fpp");

Null 传到运算符(?.)

相当于判空操作

Symbol

Symbol函数不能使用 new 命令 否则会报错 因为Symbol是一个原始类型的值 不是对象 所以不能添加属性 是一种类似于字符串的数据类型

Symbol函数的参数只表示对当前Symbol值的描述 因此相同参数的Symbol函数的返回值是不相等的

Symbol值不能与其他值进行比较 否则会报错 Symbol值可以转为字符串或者布尔值 但是不能转为数值

作为属性名的 Symbol

Symbol值可以作为标识符用于对象的属性名 复制某一个键不小心被重写或者覆盖

1
2
3
4
5
6
7
const mySymbol = Symbol();
const data = {
[mySymbol]: "james",
};
//Symbol作为对象属性名时不能使用点运算符 因为点运算符后面总是字符串 所以不会读取mySymbol作为标识名所指代的值 导致a的属性名实际上时一个字符串 而不是一个Symbol值
data.mySymbol; //undefined
data[mySymbol]; //james

Symbol值作为属性名时 该属性还是公开属性 不是私有属性

常量使用 Symbol 值的最大好处就是其他任何值不可能有相同的值了 因此可以保证 switch 语句按设计的方式工作

用处

消除魔法字符串

1
2
3
4
const shapeType = {
triangle: Symbol(),
};
//将多次出现的和代码耦合度高的字符串设置成Symbol降低耦合度

属性名的遍历

Symbol作为属性名 不会被for...in for...of 以及Object.keys() Object.getOwnPropertyNames()返回 但是不是私有属性 可以使用Object.getOwnPropertySymbols获取对象的所有Symbol属性名

Reflect.owbKeys()可以返回所有的类型的键名 包括常规键名和 Symbol 键名

Symbol值作为名称的属性不会被常规方法遍历得到 可以用这个特性为对象定义一些非私有但又希望只用于内部的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const size = Symbol("size");
class Collection {
constructor() {
this[size] = 0;
}
add(item) {
this[this[size]] = item;
this[size]++;
}
static sizeOf(instance) {
return instance[size];
}
}
const x = new Collection();
Collection.sizeOf(x); //0
x.add("foo");
Collection.sizeOf(x); //1
Object.keys(x); //['0']
Object.getOwnPropertyNames(x); //['0']
//size属性是一个Symbol值 所以Objct.keys(x)都无法获取它 这就造成了一种非私有的内部方法的效果

Symbol.for() Symbol.keyFor()

接收一个字符串作为参数 然后搜索有没有以该参数作为名称的Symbol 有就返回这个Symbol值 没有就新建一个以该字符串为名称的Symbol

1
2
3
4
const s1 = Symbol.for("foo");
const s2 = Symbol.for("foo");
s1 === s2; //true
//Symbol.for()与Symbol()这两种方法都会生成新的Symbol 前者会被登记在全局环境中供搜索 后者不会
1
2
3
4
const s1 = Symbol.for("foo");
Symbol.keyFor(s1); //foo
const s2 = Symbol("foo");
Symobl.keyFor(s2); //undefined

Symbol.forSymbol值登记的名字是全局环境的 可以在不同的iframeservice worker中取到同一个值

模块的 Singleton 模式

Singleton 模式指的是 调用一个类并且在任何时候都返回同一个实例

1
2
3
4
5
6
7
8
9
10
//mod.js
const FOO_KEY = Symbol.for("foo");
function A() {
this.foo = "hello";
}
if (!global[FOO_KEY]) {
global[FOO_KEY] = new A();
}
module.exports = global[FOO_KEY];
//可以保证globl[FOO_KEY]不会被无意间覆盖 但是还可以被改写

内置的 Symbol 值

  • Symbol.hasInstance

使用instanceof运算符时会调用这个方法 判断该对象是否为某个构造函数的实例

1
foo instanceof Foo 实际上调用了Foo[Symbol.hasInstance](foo)
  • Symbol.isConcatSpreadable
  • Symbol.species
  • Symbol.match
  • Symbol.replace
  • Symbol.search
  • Symbol.split
  • Symbol.iterator
  • Symbol.toPrimitive
  • Symbol.toStringTag
  • Symbol.unscopables

Set 和 Map

Set

Set:一组不会重复的数组

1
[...new Set(array)]; //数组去重

Set加入值时不会发生类型转换 Set内部判断两个值是否相等时使用的算法类似于精确运算符 但是NaN等于自身 (两个对象总是不相等的

Set遍历顺序就是插入顺序 这个特性非常有用 比如使用Set保存一个回调函数列表 调用时能保证按照添加顺序调用

遍历的应用

扩展运算符(...)内部使用for...of循环 也可以使用Set结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let set = new Set(["red", "blue"]);
let arr = [...set];
//扩展运算符和Set结构相结合就能去除数组的重复裁员
let unique = [...new Set(arr)];
//数组的map和fillter方法也可以用于Set
set = new Set([...set].map((x) => x * 2));
set = new Set([...set].fillter((x) => x % 2 == 0));
//所以set很用以实现并集、焦急和差集
//并集
let union = new Set([...a, ...b]);
//交集
let intersect = new Set([...a].fillter((x) => b.has(x)));
//差集
let defference = new Set([...a].fillter((x) => !b.has(x)));
1
2
3
//无法直接在遍历操作中同步改变Set结构 一种是利用原Set映射出一个新的结构 然后赋值给原来的Set结构 另一种是利用Array.from方法
set = new Set([...set].map((val) => val * 2));
set = new Set(Array.from(set, (val) => val * 2));

WaekSet

  • WeakSet的成员只能是对象 而不能是其他类型的值
  • WeakSet中的对象都是弱引用 即垃圾回收机制不考虑WeakSet对该对象的引用(如果其他对象不再引用该对象 那么垃圾回收机制就会自动回收该对象所占用的内存 不考虑是否还存在于WeakSet中)
  • WeakSet不能遍历 因为成员都是弱引用 随时可能消失 遍历机制无法保证成员存在 很可能刚刚遍历结束 成员就获取不到了

WeakSet的一个用处是储存 DOM 节点 而不用担心这些节点从文档移除时会语法内存泄漏

Map

  • Map的键可以是任何数据类型
  • Map的键实际上适合内存绑定的 只要内存不一样 就视为两个键 这就解决了同名属性的问题 我们扩展别人的库是 如果使用对象作为键名 不 i 用担心自己的属性和原作者的属性同名

只有对同一对象的引用 Map结构才将其视为同一个键

1
2
3
const map = new Map();
map.set(["a"], 555);
map.get(["a"]); //undefined
1
2
3
//如果Map的键是一个简单类型的值(数字、字符串、布尔值) 只要两个值严格相等 Map就将其视为一个键 将NaN视为一个键
map.set(NaN, 123);
map.get(NaN); //123

Map的遍历顺序就是插入顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Map转为JSON
//键名都是字符串
function strMapToJson(strMap) {
return JSON.stringify(strMapToJson(strMap));
}
//Map键名有非字符串
function mapToArrayJson(map) {
return JSON.stringify([...map]);
}
//JSON转为Map
//正常情况下 所有键名都是字符串
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr));
}
//JSON就是一个数组 且每个成员本身又是一个具有两个成员的数组
function jsonToMap(jsonStr) {
return new Map(JSON.parse(jsonStr));
}

WeakMap

如果想要往对象中添加数据又不想干扰垃圾回收机制 便可以使用WeakMap 比如在网页的DOM元素上添加数据时就可以用WeakMap结构 当该DOM元素被清除时 其对应的WeakMap记录就会自动清除

WeakMap弱引用的只是键名而不是键值 键值依然是正常引用的

将监听函数反正该WeakMap里面 一旦DOM对象消失 与它绑定的函数也会自动消失

WeakMap也可以用来部署私有属性

Proxy

要使Proxy起作用 必须针对Proxy实例进行操作 而不是针对目标对象进行操作

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

1
2
3
4
5
6
7
8
9
10
11
const proxy = new Proxy(
{},
{
get: function (target, property) {
return 35;
},
}
);
let obj = Object.create(proxy);
obj.time; //35
//proxy对象是obj对象的原型 obj对象本身没有time属性 所以根据原型链会在proxy对象上读取该属性 导致被拦截
  • get(target, propKey, receiver):拦截对象属性的读取
1
2
3
4
5
6
7
8
9
10
//get方法可以继承
let proto = new Proxy ( { } , {
get(target, propertyKey , receiver) {
console.log ( ’ GET ’+propertyKey);
return target[propertyKey] ;
.
, } )
let obj = Object.create(proto) ;
obj.xxx IIGET xxx ”
//拦截操作定义在Propertype对象上 所以如果读取obj对象继承的属性 拦截会生效
  • set(target, propKey, value, receiver):拦截的对象属性的设置

如果一个属性不可配置或不可写 则该属性不能被代理 通过Proxy对象访问该属性将会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//有时候 我们会在对象上设置内部属性 属性名的第一个字符使用下划线开头 表示这些属性不应该被外部使用 结合get和set方法可以防止这些内部属性被外部读/写
const invariant = (key, action) => {
if (key[0] === "_") {
//读取属性的第一个字符判断是否为内部属性
throw new Error(`${action}的属性${key}是内部的`);
}
};
const handler = {
get(target, key) {
invariant(key, "get");
return target[key];
},
set(target, prop, value) {
invariant(prop, "set");
target[prop] = value;
return true;
},
};

如果目标对象自身的某个属性不可写也不可配置 那么 set 不得改变这个属性的值 只能返回同样的值 否则报错

  • has(target, propKey):拦截propKey in proxy的操作
1
2
3
4
5
6
7
8
9
10
11
12
//使用has方法隐藏了某些属性 使其不被in运算符发现
const handler = {
has(target, key) {
if(key[0] === '_') {
return false;
}
return key in target
}
}
var target = { prop : ’ foo ’, prop :’foo ’ };
var proxy = new Proxy(target , handler);
’ _prop ’ in proxy // false

如果原对象不可配置或禁止扩展 那么has拦截会报错

has 方法拦截的是 Has Property 操作,而不是 HasOwnProperty 操作,即 has 方法 不判断一个属性是对象自身的属性还是继承的属性 对for...in不生效

  • deleteProperty(target, propKey):拦截delete proxy[propKey]的操作
  • ownKeys(target):拦截Object.getOwnPropertyNames(proxy) Object.getOwnPropertySymbols(proxy) Object.keys(proxy) 返回一个数组 该方法返回目标对象自身属性的属性名 而Object.keys()的返回结果仅包括目标对象自身的可遍历属性
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey)
  • defineProperty(target, propKey, propDesc):拦截 Object defineProperty(proxy propKey, propDesc 〕、 Object define Properties(proxy, propDescs)
  • preventExtensions(target):拦截 Object preventExtensions proxy
  • getPrototypeOf(target)
  • isExtensible(target)
  • setPrototypeOf(target, proto) :拦截 Object setPrototypeOf proxy proto ), 返回一个布尔值。 果目标对象是 函数 那么还有两 操作可以拦截。
  • apply(target, object, args)

apply方法拦截函数的调用/call/apply操作(直接调用Reflect.apply方法也会被拦截)

1
2
3
4
5
6
7
8
9
10
11
//三个参数 目标对象 目标对象的上下文对象 目标对象的参数数组
const target = () => {
return "i am the target";
};
const handler = {
apply() {
return "i am the proxy";
},
};
const p = new Proxy(target, handler);
p(); // i am the proxy
  • construct( target, args):拦截 Proxy 实例作为构造函数调用的操作 ,比 ηew proxy ( . . . arg)
1
2
3
4
5
6
7
8
9
target:目标对象 args:构建函数的参数对象
const p = new Proxy(function() {}, {
construct: function(target, args) {
console.log('call' + args.join(','));
return {value: args[0]*10}
}
});
(new p(1)).value;//10
//construct方法返回的必须是一个对象 否则会报错

this 问题

Proxy代理的情况下 目标对象内部的this关键字会指向Proxy代理

1
2
3
4
5
6
7
8
9
10
const target = {
m() {
console.log(this === proxy);
},
};
const handler = {};
const proxy = new Proxy(target, handler);
target.m(); //false
proxy.m(); //true
//target内部的this也指向proxy 而不是target

有些元素对象的内部属性只有通过正确的this才能获取 所以Proxy也无法代理这些原生对象的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);
proxy.getDate();//TypeError: this is not a Data object
//这个时候绑定bind就能解决问题
const target = new Date (’ 2015-01-0 1 ’) ;
const handler = {
get(target, prop) {
if (p rop === ’ get Date ’) {
return target . getDate .bind(target) ;
return Reflect . get(target, prop);
}
};
const proxy = new Proxy(target, handler);
proxy.getDate()//1

实例:web 服务的客户端

Proxy对象可以拦截目标对象的任意属性 所以它很适合编写 Web 服务的客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
const service = createWebSer rice (’ http://example . com/data ’);
service . employees() . then(json => {
const employees= JSON . parse(json) ;
//
}) ;
//proxy可以拦截这个对象的任意属性 所以不用为每一种数据写一个适配方法 只要写一个proxy拦截即可
function createWebService(baseurl) {
return new Proxy({}, {
get(target, propKey, receiver) {
return () => httpGet(baseUrl + '/' + propKey)
}
})
}

Proxy还可以用来实现数据库的ORM

Reflect

  • Object对象的一些明显属于语言内部的方法(Object.defineProperty)放到Reflect对象上
  • 修改某些Object方法的返回结果 让其变得合理
1
2
3
4
5
6
7
8
9
10
11
12
//旧写法
try {
Object.defineProperty(target, property, attributes);
} catch (e) {
//failure
}
//新写法
if (Reflect.defineProperty(target, property, attributes)) {
//success
} else {
//failure
}
  • object操作编程函数行为
1
2
3
4
//旧写法
"assign" in Object; //true
//新写法
Reflect.has(Object, "assign"); //true
  • Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就 能在 Reflect 对象上找到对应的方法。这就使 Proxy 对象可以方便地调用对应的 Reflect 方法来完成默认行为,作为修改行为的基础。也就是说,无论 Proxy 怎么修改默认行为,我们 总可以在 Reflect 上获取默认行为。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Proxy(target, {
set(target, name, value, receiver) {
const success = Reflect.set(target, name, value, receiver);
if (success) {
//;;;;;;;
}
return success;
},
});
//Proxy拦截了target对象的属性赋值方法 他采用Reflect.set方法将之际赋给对象的属性 保证原有的行为 然后再部署额外的功能
var loggedobj = new Proxy(obj, {
get(target, name) {
console.log("get", target, name);
return Reflect.get(target, name);
},
deleteProperty(target, name) {
console.log("delete" + name);
return Reflect.deleteProperty(target, name);
},
has(target, name) {
console.log("has" + name);
return Reflect.has(target, name);
},
});
//每个proxy对象的拦截操作都内部调用了Reflect对应的方法 保证原生行为能够正常运行 添加的工作就是将每一个操作输出一行日志

image-20220505222048711

使用 Proxy 实现观察者模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const queuedObservers = new Set(); //观察者函数容器
const set = (target, key, value, receiver) => {
const result = Reflect.set(target, key, value, receiver); //执行正常set操作
queuedObservers.forEach((observer) => observer()); //执行观察者容器里所有观察者
return result;
};
const observe = (fn) => queuedObservers.add(fn); //加入观察者容器
const observable = (obj) => new Proxy(obj, { set }); //设置拦截器
const person = observable({
name: "张三",
age: 20,
});
const print = () => {
console.log("dsqdqdqdsqd");
};
observe(print);
person.name = "curry";

Promise

  • 对象的状态不受外界影响
  • 一旦状态改变就不会再变 任何时候都可以得到这个结果
  • 无法取消Promise 一旦新建就会立即执行 无法中途取消
  • 如果不设置回调函数 Promise内部抛出的错误不会反映到外部
  • 处于Pendding状态时 无法得知目前进展到哪一阶段
  • 如果某些事件不断重复发生 使用Stream模式更好
1
2
3
4
5
6
7
8
9
10
//Promise新建后会立即执行
let promise = new Promise((resolve, reject) => {
console.log("Promise");
resolve();
});
promise.then(() => {
console.log("Resolved");
});
console.log("HI"); //Promise HI Resolved
//Promise新建后立即执行 然后then指定的回调函数会在当前脚本所有同步任务执行完成后才执行 所以resolved最后输出

调用resolvereject并不会终结Promise的参数函数的执行

1
2
3
4
5
new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then((e) => console.log(r)); //2 --> 1
//立即resolved的Promise是在本轮事件循环的末尾执行 总是晚于本轮循环的同步任务 一般应该再resolve和reject前面加上return 防止后续还有操作
1
2
3
4
5
6
7
8
9
10
11
var promise = new Promise(function (resolve , reject) {
resolve (’ ok ’) ;
throw new Error ( ’ test ’);
.
, } )
promise
.then (function(value) { console.log(value) })
. catch (function (error) { console . log (error) } ) ;
11 ok
//Promise再resolve语句后面再抛出错误 并不会被捕获 等于没有抛出 因为Promise的状态一旦改变 就会永久保持该状态 不会再改变了

Promise对象的错误具有冒泡性质 会一直向后传递知道被捕获为止 即错误总会被下一个catch捕获

不要再 then 中定义reject状态的回调函数 而应该总是使用catch方法

如果没有指定 catch 方法指定错误处理的回调函数 Promise 对象抛出的错误不会被传递到外层代码 即不会有任何反应


文章作者: olddog
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 olddog !
  目录