ES6学习记录——上

这里写目录标题

  • let与const
  • 块级作用域
  • 顶层对象的属性
  • 解构赋值
    • 数组解构赋值
    • 对象解构赋值
    • 其他数据格式的解构赋值
      • 解构赋值常见用途
    • 字符串扩展
      • 新增方法
    • number类型扩展
      • 新增方法
      • BigInt类型
    • 函数的扩展
      • 函数默认值
      • ...rest
      • name属性
      • 箭头函数
    • 尾调用
      • 尾调用优化
      • 递归中的尾调用优化
    • 数组类型的扩展
      • 扩展运算符
      • 数组的方法
        • 1.Array.from()
          • 2.Array.of()
        • 3.copyWithin()
        • 4.find()
        • 5.findIndex()
        • 6.fill()
        • 7.entries(),keys() 和 values()
        • 8.includes()
        • 9.flat()
        • 10.flatMap()
        • 11.at()
    • 对象的扩展
      • 属性的遍历
      • 属性的可枚举性
      • super关键字
      • 对象的解构赋值与扩展运算符
      • 对象新增方法
        • 1. `Object.is(a,b)`
        • 2.`Object.assign()`
        • 3.`Object.getOwnPropertyDescriptors()`
        • 4.`Object.values()`
        • `Object.entries()`
        • `Object.fromEntries()`
      • 原型对象的操作方法
        • `Object.setPrototypeOf()`
        • `Object.getPrototypeOf() `
    • 运算符扩展
      • 指数运算符`**`
      • 链判断运算符`?.`
      • 空值判断运算符`??`
      • 逻辑赋值运算符`||=、&&=、??=`
    • Symbol
      • 对象中使用
      • 属性的遍历
      • `Symbol.for()`和`Symbol.keyFor() `
    • Set
      • Set的属性与方法
      • WeakSet
    • Map
      • Map的属性和操作方法
      • 与其他数据结构的互相转换
      • WeakMap
      • WeakRef
    • Proxy
      • Proxy支持的代理操作
      • Proxy.revocable()
    • Reflect

let与const

  • let定义变量有作用域

for循环中设置循环变量的部分是一个父作用域,循环体部分是一个子作用域

var a = [];
for (var i = 0; i < 10; i++) {
//如果这里用var来定义i 那么全局就只有这一个i 
  a[i] = function () {
    console.log(i);
  };
}
//循环体外调用console的时候,全局的i已经是10了
a[6](); // 10

如果使用let定义i就可以避免这个问题,因为每次循环定义的i都是新的作用域

  • 禁止变量提升

变量提升:变量可以在var定义之前就使用,不过值为undefined

  • 暂时性死区
    如果一个块级作用域中,通过let定义过一个变量,那么改变量将无视外部影响(调用或修改),减少运行时的报错。
var tmp = 123
if(true){
	tmp = '123' //referenceError 死区
	let tmp
	tmp = '123' //123
}

如果在暂时性死区内使用typeof 操作符,则会报错refenerceError。如果单纯typeof一个未定义变量,会返回undefined

  • 禁止重复声明
    let禁止在同一块级作用域中重复声明参数

  • const
    const用于定义常量,不允许修改,在定义时就必须赋值。其性质与let相同。在定义基本数据类型时不可变,定义对象的时候,由于储存的是对象的引用,因此对象可以进行修改。如果希望定义的对象也无法修改,可以使用object.freeze(obj)

块级作用域

在ES5中,只有全局作用域与函数作用域,这会有一些问题。

var tmp = new Date();

function f() {
//变量提升导致这里打印了函数内部作用域的tmp = undefined
  console.log(tmp);
  if (false) {
    var tmp = 'hello world';
  }
}

f(); // undefined

let生成的块级作用域可以任意嵌套,父层级无法读取子层级的变量,相反可以。
?块级作用域的出现,可以替代立即执行函数表达式。

(function () {
  var tmp = ...;
  ...
}());

// 块级作用域写法
{
  let tmp = ...;
  ...
}

块级作用域中尽量使用函数表达式定义函数,而不是函数声明的方式,因为在浏览器的运行环境中,为了兼容性规定:

  • 允许在块级作用域内声明函数。
  • 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
  • 同时,函数声明还会提升到所在的块级作用域的头部。

此时函数声明当做var定义的undefined变量,并将其提升到函数作用域与全局作用域的头部,导致报错xxx is not a function

// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    function f() { console.log('I am inside!'); }
  }
  f();// Uncaught TypeError: f is not a function
}());

///等同于
function f() { console.log('I am outside!'); }

(function () {
  var f = undefined
  if (false) {
    function f() { console.log('I am inside!'); }
  }
  f();// Uncaught TypeError: f is not a function
}());

顶层对象的属性

浏览器:window
node:global
Web Worker:self
ES2020,引入globalThis来获取顶层对象

在ES5中 var、function 定义的全局变量会被当做是顶层对象的属性,而使用let、const、class定义的变量将不会当做顶层对象的属性。

var a = 1;
window.a // 1

let b = 1;
window.b // undefined

顶层对象的属性与全局变量挂钩,被认为是JavaScript语言最大的设计缺陷,这将导致编译时无法判断一个变量是未声明还是错误输入,因为全局对象可能是顶层对象的属性,而属性随时都可以动态添加。

解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。

数组解构赋值

写法的本质上就是格式匹配。

let [a, b, c] = [1, 2, 3];

如果格式匹配不上,结构的变量值为undefined

let [bar, foo] = [1];// foo = undefined

只要数据结构具有Iterator接口,就可以进行解构,否则会报错

let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
//以上都报错

解构可以指定默认值,但只有被解构的数组元素严格等于undefined,默认值才会生效

let [a=1] = [] //a=1
let [a=1] = [null] //a=null

默认值可以引用解构赋值的其他变量,但该变量必须已经声明

let [a=1,b=a]=[] // a=1 b=1
let [a=b,b=1]=[] // ReferenceError: b is not defined

对象解构赋值

与数组不同,对象的解构赋值不是按照位置顺序,而是根据对象属性名称来匹配

let {a,b} = {b:1,a:2}
//等价于
let {a:a,b:b} = {b:1,a:2} //b=1,a=2

当属性名称无法对应到被解构对象中时,解构失败,变量值为undefined。如果确定要赋值某个属性,也可以通过key:value来解构。

let {c} = {a:1,b:2} // c=undefined 
let {a:c} = {a:1,b:2} //c=1  

对象的解构赋值也可以用于嵌套结构

const node = {
  loc: {
    start: {
      line: 1,
      column: 5
    }
  }
};
//这里,第一个loc是变量可以赋值,后面的都只是标识结构
let { loc, loc: { start }, loc: { start: { line }} } = node;
line // 1
loc  // Object {start: Object}
start // Object {line: 1, column: 5}

对象的解构赋值可以取到继承的属性

const obj1 = {};
const obj2 = { foo: 'bar' };
Object.setPrototypeOf(obj1, obj2);//定义obj1继承自obj2

const { foo } = obj1;
foo // "bar"

对象结构可以制定默认值,与数组相同,如果默认值要生效,则被解构的对象对应属性必须严格等于undefined

注意,如果将已声明的变量用于结构复制,避免将{}用于行首,因为 JavaScript 引擎会将{}理解成一个代码块,从而发生语法错误。

let x;
{x} = {x: 1}; // SyntaxError: Unexpected token '='
({x} = {x: 1}); //x=1

其他数据格式的解构赋值

只要等号右边的值不是对象或数组,就先将其转为对象

//string
const [a, b, c, d, e] = 'hello';
let {length : len} = 'hello'; //去了类数组的length对象
len // 5
//number
let {toString: s} = 123;
s === Number.prototype.toString // true
//boolean
let {toString: s} = true;
s === Boolean.prototype.toString // true
// undefined与null无法转化为对象
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError

函数参数的解构赋值

//这样的写法是为x和y定义初始值,并设置参数初始值为{} 如果没有传入,就会按初始处理
function move({x = 0, y = 0} = {}) {
  return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]

====================
//这种写法并没有初始化xy的值,而是设置一个初始参数来对xy进行解构赋值
//如果传入的参数不包含xy,则无法解构对应参数,返回undefined
function move({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]

解构赋值常见用途

  1. 交换变量值
  2. 方便提取对象中的数据
  3. 函数参数设置默认值
  4. 遍历Map结构
// 获取键名
for (let [key] of map) {
  // ...
}

// 获取键值
for (let [,value] of map) {
  // ...
}

字符串扩展

  1. 模板字符串 ${}
  2. 加强了对 Unicode 的支持。通过括号可以解读超过0xFFFF的数值
  3. 字符串添加了遍历器接口,使其可以调用for ... of
  4. 允许输入反斜杠、回车、行分隔符、段分隔符、换行符等符号的uicode

新增方法

  1. String.fromCodePoint() 用于返回Unicode码点对应的字符。对应ES5String.fromCharCode()。能够识别超过0xFFFF的数值。
  2. String.raw()处理模板字符串,获取原始字符串。占位符${}中的内容会被计算出来,但是转义符```不会生效,而是原样输出。
// 一般用法
String.raw`hello\nw${0}rld` //hello\nworld
// 也可以当做函数使用,第一个参数为占位符分割的字符串,剩余参数为内嵌表达式的值
String.raw({raw:['hello\nw','rld']},0)
  1. codePointAt() (实例方法)用于返回Unicode 编码点值的非负整数。能能够正确识别4字节储存的字符。参数是字符在字符串中的位置(从 0 开始)。
let s = '';
s.charCodeAt(0) // 55362  只能返回前两个字节对应数值
s.codePointAt(0) // 134071  正确返回完整数值

JavaScript 内部,字符以 UTF-16 的格式储存,每个字符固定为2个字节。对于Unicode 码点大于0xFFFF的字符需要4个字节来储存

  1. normalize()(实例方法)规范字符串编码
  2. includes()(实例方法)判断是否包含字符串,传入两个参数,查找的字符串和开始的位置。返回布尔值
  3. startsWith()(实例方法)判断字符串开头,传入两个参数,查找的字符串和开始的位置。返回布尔值
  4. endsWith()(实例方法)判断字符串结尾,传入两个参数,查找的字符串和前几个字符。返回布尔值
  5. repeat()(实例方法),返回重复n次的字符串,n参数,n>-1。
  6. padStart() 用于头部补全,padEnd()用于尾部补全(实例方法),接受两个参数,第一个是补全后的最大长度,第二个是补全的内容。
// 补全字符串到指定位数
'12'.padStart(10, '0') // "0000000012"
// 提示字符串格式
'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
  1. trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格。返回新字符串。
  2. replaceAll()一次替换所有匹配的字符。返回新字符串
'aabbcc'.replace('b', '_') // aa_bcc
'aabbcc'.replaceAll('b', '_') // aa__cc
  1. at()接受一个整数参数,返回指定位置的字符,支持负值(倒数)

number类型扩展

数字中可以通过_来对位数进行分隔。不能放在开头或末尾,对于 JavaScript 内部数值的存储和输出,并没有影响。

12345_00 === 123_4500 // true

新增方法

  1. Number.isFinite() 用来检查一个数值是否有限
  2. Number.isNaN()用来检查值是否为NaN
  3. Number.parseInt() 将参数转化为整数
  4. Number.parseFloat() 将参数转化为浮点数
  5. Number.isInteger() 判断一个数是否为整数,小数点较多超出16位时,会误判。
  6. Number.EPSILON代表1 与大于 1 的最小浮点数之间的差,用于标识JavaScript 能够表示的最小精度。
  7. Number.isSafeInteger()判断一个整数是否在JS能够识别的范围内,-253到253

由于JS浮点数是取了近似值,因此在计算的时候会出现误差,当误差小于Number.EPSILON时,JS就认为不存在误差了。
ES6 引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。

BigInt类型

为了识别超出JS范围的大数,使用数值后面加n的方式来表示

42n === 42 // false
typeof 123n // 'bigint'
-42n // 正确
+42n // 报错
// BigInt()函数将其他数据类型转换为BigInt
BigInt(123) // 123n
BigInt('123') // 123n
BigInt(false) // 0n
BigInt(true) // 1n

函数的扩展

函数默认值

ES6以后,可以对函数参数制定默认值,指定方式类似TS,直接在参数后加=

  1. 通常定义默认值的参数都放在末尾。如果非末尾的参数设置了默认值,那该参数就无法省略
  2. 函数的length属性,返回未设置默认值的参数的个数。
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
(function (a, b = 1, c) {}).length // 1

如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。
并且length属性不会计入rest参数

  1. 如果为函数参数设置了默认值,函数在声明初始化的时候会形成一个单独的作用域,初始化结束的时候消失。
var x = 1;
// 这里参数y默认值为x,生成单独的作用域
function f(x, y = x) {
  console.log(y);
}
//这时生成的作用域中x=2,如果作用域中找不到x的值,才会去外层找
f(2) // 2 
f() // 1

…rest

用于获取函数的多余参数,将多余的参数作为数组传入。

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

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

name属性

返回函数名

var f = function () {};

// ES5
f.name // ""

// ES6
f.name // "f"

箭头函数

  1. 箭头函数没有自己的this对象,普通函数this在运行时制定,箭头函数this就是定义时上层作用域的this
  2. 不可当做构造函数,不可对箭头函数使用new命令
  3. 不可以使用arguments对象。可用rest代替
  4. 不可以使用yield命令,因此箭头函数不能用作Generator函数

下面两种情况不能使用箭头函数

  1. 如果一个对象属性定义的函数中需要使用this,则不能使用箭头函数来定义该属性,因为对象不够成单独的作用域。
  2. 如果需要动态获取this的时候
const cat = {
  lives: 9,
  //这里的箭头函数this会指向全局作用域
  jumps: () => {
    console.log(this.lives);
  }
}
cat.jump() // undefined 
var button = document.getElementById('press');
//这里如果使用箭头函数,则this会指向全局对象,无法获取到当前点击的对象
button.addEventListener('click', () => {
  this.classList.toggle('on');
});

尾调用

尾调用就是某个函数的最后一步仅仅是另一个函数的调用。

function f(x) {
  if (x > 0) {
  //不一定是函数末尾,但在逻辑上是最后一步
  //仅仅是调用,没有任何其他操作
    return m(x)
  }
  return n(x);
}

尾调用优化

即只保留内层函数的调用帧,只能在严格模式下使用。
尾调用是函数的最后一步操作,如果内层函数是本次函数调用栈的尾调用,可以直接用内层函数的调用帧,取代外层函数的调用帧。引擎不需要对下一个函数调用创建一个新的栈帧,只需复用已有的栈帧。

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

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

// 等同于
g(3);

递归中的尾调用优化

递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误。如果我们将递归修改为尾调用的形式,只存在一个调用帧,就可以避免报错。

//非尾调用
function Fibonacci (n) {
  if ( n <= 1 ) {return 1};

  return Fibonacci(n - 1) + Fibonacci(n - 2);
}
//尾调用
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
  if( n <= 1 ) {return ac2};

  return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}

数组类型的扩展

扩展运算符

扩展运算符(spread)是三个点...。类似rest的逆运算,是吧数组作为按逗号分割的参数序列。

console.log(...[1, 2, 3]) //1,2,3
const a2 = [...a1]; // 复制数组 浅拷贝
[...arr1, ...arr2, ...arr3] // 合并数组 浅拷贝
let [arr1, ... arr2] = [1,2,3] //结构赋值时,只能放在最后
[...'hello'] // [ "h", "e", "l", "l", "o" ] 直接将字符串转换为数组

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

数组的方法

1.Array.from()

可以将一个类数组对象或者是定义了遍历器(Iterator)接口的结构转换为新的数组

类数组对象,就是指有length属性的对象,典型的有NodeList对象和arguments

有三个参数,第一个为传入要转化的结构,第二个是一个函数,类似于map,可以对每个元素进行处理,第三个参数可以绑定传入第二个函数中的this

Array.from(string).length // 和扩展预算符一样,可以用作将字符串转化为数组
Array.from({ length: 2 }, () => 'jack') // 这样的写法可以控制函数参数执行的次数
2.Array.of()

用于将一组值,转换为数组。
由于构造函数Array()在传入一个参数的时候会将参数识别为数组长度,为了解决这个冲突,就定义了Array.of()定义数组

Array(3) // [, , ,]
Array.of(3) // [3]

3.copyWithin()

实例方法,在当前数组内部,将指定位置的成员复制到其他位置,它接受三个参数。

  • target(必需):从该位置开始替换数据。如果为负值,表示倒数。
  • start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。
  • end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。
[1, 2, 3, 4, 5].copyWithin(0, 3) //[4,5,3,4,5]

4.find()

用于找出数组中第一个符合条件的元素。
接受一个参数,是一个函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。

5.findIndex()

返回数组中第一个符合条件的元素的位置。

6.fill()

用于填充数组,接收三个参数,第一个是用于填充的值,后两个是填充起始与终止位置(非必传),如果第一个参数为对象,那么填充的只是对象的引用(浅拷贝)

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

7.entries(),keys() 和 values()

实例方法,返回数组的一个遍历器对象,用于for .. of 循环

8.includes()

返回布尔值,表示数组是否包含某个值。接受两个参数,第一个为查找的值,第二个为查找的起始位置(支持负值)。
相比indexOf方法(===判断),可以正确识别NaN

[1, 2, NaN].includes(NaN) // true

9.flat()

实例方法,用于打平多维数组。返回一个新的数组。接受一个参数,表示打平的层数,如果打平全部层数可以传入Infinity

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

10.flatMap()

实例方法,相当于map()+flat()先对数组中的每一项进行处理,再打平一次。传入一个函数作为参数。

const tree = [ 1, [2, 3]];
tree.flatMap(x=>x+1)
// 先进行x+1再打平数组
// [2, '2,31']

11.at()

实例方法,接受一个整数作为参数,返回对应位置的成员,支持负索引

对象的扩展

属性的遍历

ES6有5种遍历方式

  1. for in 用于遍历对象自身和继承的可枚举属性(不含 Symbol 属性)。
  2. Object.keys()返回对象自身可枚举属性(不含Symbol属性)的key组成的数组。
  3. Object.getOwnPropertyNames()返回对象自身所有属性(不含Symbol属性)的key值组成的数组。
  4. Object.getOwnPropertySymbols()返回对象自身所有Symbol属性key值的数组。
  5. Reflect.ownKeys(obj)返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

遵守同样的属性遍历的次序规则。
首先遍历所有数值键,按照数值升序排列。
其次遍历所有字符串键,按照加入时间升序排列。
最后遍历所有 Symbol 键,按照加入时间升序排列。

属性的可枚举性

对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。

let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
//  {
//    value: 123,
//    writable: true,
//    enumerable: true,
//    configurable: true
//  }

其中的enumerable表示该属性是否可枚举,如果该值为false,则下面四个操作会忽略该属性
1.for…in循环:只遍历对象自身的和继承的可枚举的属性。
2.Object.keys():返回对象自身的所有可枚举的属性的键名。
3.JSON.stringify():只串行化对象自身的可枚举的属性。
4.Object.assign(): 忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。

super关键字

对象中的函数this总是指向当前对象,ES6新增super关键字用来指向当前对象的原型对象。但是必须用在对象的方法之中(必须是直接声明的方法,不能通过字面量赋值的方式)。

const proto = {
  x: 'hello',
  foo() {
    console.log(this.x);
  },
};

const obj = {
  x: 'world',
  foo() {
    super.foo();
  }
}
//制定proto对象为obj对象的原型对象
Object.setPrototypeOf(obj, proto);
//由于原型对象中的foo()方法使用了this,在调用的时候是在子对象中,因此输出的是world
obj.foo() // "world"

对象的解构赋值与扩展运算符

扩展运算符用于对象中时,不能复制继承自原型对象的属性。

const o = Object.create({ x: 1, y: 2 });
o.z = 3;

let { x, ...newObj } = o;
let { y, z } = newObj;
x // 1
y // undefined 因为y是在构造函数中定义的,不是对象实例自身的属性
z // 3

解构赋值中使用扩展运算符

  1. 对undefined或null解构会报错
  2. 解构赋值中使用扩展运算符必须放在最后
let { ...x, y, z } = someObject; // 句法错误
  1. 结构赋值是浅拷贝

对象新增方法

1. Object.is(a,b)

判断两个值是否相等,相比===,能够准确判断NaN、-0、+0

// ES5 实现
Object.defineProperty(Object, 'is', {
  value: function(x, y) {
    if (x === y) {
      // 针对+0 不等于 -0的情况
      return x !== 0 || 1 / x === 1 / y;
    }
    // 针对NaN的情况
    return x !== x && y !== y;
  },
  configurable: true,
  enumerable: false,
  writable: true
});

2.Object.assign()

合并对象,接受多个对象作为参数,将源对象(非第一个参数)自身可枚举属性合并到目标对象中(第一个参数)。如果传入非对象值作为参数,则会先转换为对象。

//完整拷贝一个对象
function clone(origin) {
//获取传入对象的原型对象
  let originProto = Object.getPrototypeOf(origin);
  //通过原型对象创建一个新对象,然后在合并传入对象
  return Object.assign(Object.create(originProto), origin);
}

Object.assign()中的对象复制是浅拷贝,并且无法复制对象属性的特性。

const source = {
  foo:1
};
//这里修改source对象foo属性的特性为不可修改
Object.defineProperty(source,'foo',{writable:false})
const target1 = {};
// 将source对象拷贝到target1中
Object.assign(target1, source);
console.log(Object.getOwnPropertyDescriptor(source,'foo'))
console.log(Object.getOwnPropertyDescriptor(target1,'foo')) 
//{value: 1, writable: false, enumerable: true, configurable: true}
//拷贝后foo属性的writable又被初始化为true,相当于重新定义
//{value: 1, writable: true, enumerable: true, configurable: true}

Object.assign()只能进行值的复制,而不会拷贝它背后的赋值方法或取值方法。因此访问器属性会被转换成数据属性,再进行复制。

const source = {
  get foo() { return 1 }
};
const target = {};

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

3.Object.getOwnPropertyDescriptors()

ES5 的Object.getOwnPropertyDescriptor()方法会返回某个对象属性的描述对象(descriptor)。ES2017 引入了Object.getOwnPropertyDescriptors()方法,返回指定对象所有自身属性(非继承属性)的描述对象。
接收一个参数为要解析的对象

const obj = {
  foo: 123,
  get bar() { return 'abc' }
};

Object.getOwnPropertyDescriptors(obj)
// { foo:
//    { value: 123,
//      writable: true,
//      enumerable: true,
//      configurable: true },
//   bar:
//    { get: [Function: get bar],
//      set: undefined,
//      enumerable: true,
//      configurable: true } 
// }

解决了Object.assign()无法正确拷贝get属性和set属性的问题。

// 优化后的合并函数
//通过Object.defineProperties来为目标对象绑定属性
const shallowMerge = (target, source) => Object.defineProperties(
  target,
  //获取源对象内部可遍历属性
  Object.getOwnPropertyDescriptors(source)
);

另一个用处,是配合Object.create()方法,将对象属性克隆到一个新对象。这属于浅拷贝。

const shallowClone = (obj) => Object.create(
  Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj)
);
//这种写法也可以实现对象的继承
const obj = Object.create(
  prot,// 父对象
  Object.getOwnPropertyDescriptors({
    foo: 123,
  })
);

4.Object.values()

返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值value。忽略symbol值属性

const obj = { foo: 'bar', baz: 42 };
Object.values(obj)
// ["bar", 42]

Object.entries()

返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名与键值的二维数组

const obj = { foo: 'bar', baz: 42 };
Object.entries(obj)
// [ ["foo", "bar"], ["baz", 42] ]

该方法可以用于将对象转化为Map结构

const obj = { foo: 'bar', baz: 42 };
const map = new Map(Object.entries(obj));
map // Map { foo: "bar", baz: 42 }

Object.fromEntries()

该方法为Object.entries()的逆操作,用于将一个键值对数组转为对象。

Object.fromEntries([
  ['foo', 'bar'],
  ['baz', 42]
])
// { foo: "bar", baz: 42 }

原型对象的操作方法

__proto__属性,用来读取或设置当前对象的原型对象。该属性必须是在支持的运行环境中才能使用,有兼容性风险。因此ES6中新增了Object.setPrototypeOf()Object.getPrototypeOf()方法来操作原型对象。

Object.setPrototypeOf()

用来设置一个对象的原型对象(prototype),返回参数对象本身。

// 将A对象的原型设置为B
const a = Object.setPrototypeOf(A, B);

Object.getPrototypeOf()

用于读取一个对象的原型对象

Object.getPrototypeOf(obj);

运算符扩展

指数运算符**

let b = 4;
b **= 3;
// 等同于 b = b * b * b;

链判断运算符?.

用于判断左侧的对象是否为null或undefined。如果是,就不再往下运算,直接返回undefined。

obj?.prop // 对象属性是否存在
obj?.[expr] // 同上
func?.(...args) // 函数或对象方法是否存在

空值判断运算符??

它的行为类似||,但是只有运算符左侧的值为nullundefined时,才会返回右侧的值。
这个运算符的一个目的,就是跟链判断运算符?.配合使用,为null或undefined的值设置默认值。

逻辑赋值运算符||=、&&=、??=

先进行逻辑运算,然后根据运算结果,再视情况进行赋值运算。为属性设置默认值

// 老的写法
user.id = user.id || 1;
user.id || (user.id = 1);
// 新的写法
user.id ||= 1;

Symbol

属于JS基本数据类型之一,表示独一无二的值。

let simple = symbol()
typeof simple
// "symbol"

可以接收一个字符串作为该Symbol值的描述。通过description可以取得描述值

let s1 = Symbol('foo');
s1.description // 'foo'
// Symbol值可以转化为字符串或是boolean类型
String(s1) or s1.toString() //'Symbol(foo)'
Boolean(s1) // true

对象中使用

由于Symbol值是不重复的,用于对象的属性名就可以避免属性名出现重名的情况。

Symbol 值作为对象属性名时,不能用点运算符。
在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。
Symbol 值作为属性名时,该属性还是公开属性,不是私有属性。

let mySymbol = Symbol();
let a = {
  [mySymbol]: 'Hello'
};
a[mySymbol]// 'Hello'

可以用于消除魔术字符串(将函数中有逻辑耦合的字符串提取到外部,用一个变量代替),由于只需要保证提取的字符串唯一,就可以在对象中将该字符串属性设置为一个Symbol值。

属性的遍历

Symbol 作为属性名,遍历对象的时候,该属性不会出现在for...in、for...of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。

但是,它也不是私有属性,有一个Object.getOwnPropertySymbols()方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。

const obj = {};
let a = Symbol('a');
let b = Symbol('b');

obj[a] = 'Hello';
obj[b] = 'World';

const objectSymbols = Object.getOwnPropertySymbols(obj);

objectSymbols
// [Symbol(a), Symbol(b)]

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

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

Symbol.for()Symbol.keyFor()

Symbol.for()可以获取到同一个Symbol值。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局。

let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
let s3 = Symbol('bar')
let s4 = Symbol('bar')
s1 === s2 // true
s3 === s4 // false

Symbol.for()Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。
Symbol.keyFor()方法返回一个已登记的 Symbol 类型值的key。

let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"

let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined

Set

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

const s = new Set();


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

Set的属性与方法

  • Set.prototype.constructor:构造函数,默认就是Set函数。
  • Set.prototype.size:返回Set实例的成员总数。
  • Set.prototype.add(value):添加某个值,返回 Set 结构本身。
  • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。
  • Set.prototype.clear():清除所有成员,没有返回值。
  • Set.prototype.keys():返回键名的遍历器。
  • Set.prototype.values():返回键值的遍历器。由于Set对象的键名和键值是同一个值,因此所以keys方法和values方法的行为完全一致。
  • Set.prototype.entries():返回键值对的遍历器。
for (let item of set.entries()) {
  console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]
  • Set.prototype.forEach():使用回调函数遍历每个成员。
let set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + ' : ' + value))
// 1 : 1
// 4 : 4
// 9 : 9

WeakSet

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有一些区别。

  1. WeakSet 的成员只能是对象,而不能是其他类型的值。
  2. WeakSet 成员是对象的弱引用,当JS垃圾回收机制回收这些对象时,不会检测到这里的调用。因此,WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。
  3. 由于 WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定
    WeakSet 不可遍历。
const a = [[1, 2], [3, 4]];
//可以接受一个数组作为参数来初始化,但是数组中的成员必须都为对象,否则会报错
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}

WeakSet 结构有以下三个方法。

  • WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。
  • WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。
  • WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例之中。

Map

Map结构类似于对象,但是其键名不局限为一个字符串,还可以是对象等其他数据类型。
任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构(详见《Iterator》一章)都可以当作Map构造函数的参数。用来生成一个Map。
如果对同一个键多次赋值,后面的值将覆盖前面的值。

const m = new Map();
const o = {p: 'Hello World'};

m.set(o, 'content')
m.get(o) // "content"

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

const map = new Map();

map.set(['a'], 555);
//这里set和get中的['a']虽然写法完全一样,但却是两个独立的数组
map.get(['a']) // undefined  

Map的属性和操作方法

  • size属性返回 Map 结构的成员总数。
  • Map.prototype.set(key, value)设置键名key的值为valuie,然后返回整个 Map 结构,如果key已经有值,则键值会被更新,否则就新生成该键。可以采用链式写法。
  • Map.prototype.get(key)获取键名为key的值,如果找不到key,返回undefined。
  • Map.prototype.has(key)返回一个布尔值,表示某个键是否在当前 Map 对象之中。
  • Map.prototype.delete(key)删除某个键,返回true。如果删除失败,返回false。
  • Map.prototype.clear()清除所有成员,没有返回值。

遍历方法,返回遍历器,可用for of拿到具体的值

  • Map.prototype.keys():返回键名的遍历器。
  • Map.prototype.values():返回键值的遍历器。
  • Map.prototype.entries():返回所有成员的遍历器。
  • Map.prototype.forEach():遍历 Map 的所有成员。

与其他数据结构的互相转换

  1. Map 转为数组最方便的方法,就是使用扩展运算符(…)。将数组传入 Map 构造函数,就可以转为 Map。
  2. 如果所有 Map 的键都是字符串,它可以无损地转为对象。对象转为 Map 可以通过Object.entries()。
let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj));

WeakMap

类似WeakSet,只能接受对象作为键名,WeakMap的键名所指向的对象,不计入垃圾回收机制。可以防止内存泄漏。

WeakRef

WeakSet 和 WeakMap 是基于弱引用的数据结构,ES2021 更进一步,提供了 WeakRef 对象,用于直接创建对象的弱引用。

let target = {};
let wr = new WeakRef(target);//定义一个对象的弱引用对象,垃圾回收机制不会计入这个引用。

该实例对象有一个方法deref(),用于返回原始引用的对象。如果原始对象已经被垃圾回收机制清除,该方法返回undefined

let target = {};
let wr = new WeakRef(target);

console.log(wr.deref() === target); // true

标准规定,一旦使用WeakRef()创建了原始对象的弱引用,那么在本轮事件循环(event loop),原始对象肯定不会被清除,只会在后面的事件循环才会被清除。

Proxy

proxy可以拦截对对象的访问,可以对外界的访问进行过滤和改写。

//作为构造函数,Proxy()接收两个参数,第一个是拦截的目标对象,第二个是描述代理的操作的对象
var proxy = new Proxy({}, {
//这里拦截了读取属性的操作,传入两个参数,第一个是拦截的目标对象,第二个是要访问的属性。
  get: function(target, propKey) {
  //无论读取什么属性,都返回一个固定值35
    return 35;
  }
});
proxy.time // 35
proxy.name // 35

用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

Proxy支持的代理操作

ES6学习记录——上_第1张图片

Proxy.revocable()

方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。上面代码中,当执行revoke函数之后,再访问Proxy实例,就会抛出一个错误。

let target = {};
let handler = {};

let {proxy, revoke} = Proxy.revocable(target, handler);

proxy.foo = 123;
proxy.foo // 123

revoke();
proxy.foo // TypeError: Revoked

Reflect

Reflect对象和Proxy对象都是ES6为了操作对象提供的新API。主要目的为

  1. 将一些Object上的属于语言内部的方法(比如Object.defineProperty)转移到Reflect上。目前这两个关键字都可以调用这些方法,后续新方法将只部署在Reflect对象上。
  2. 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false
  3. 使Object上的操作都变成函数式,比如name in objdelete obj[name],可以写作Reflect.has(obj, name)Reflect.deleteProperty(obj, name)
  4. Proxy对象中的方法一一对应。配合使用可以在不影响对象原本的行为的情况下,部署额外的功能。比如观察者模式
// 一个观察者模式的实例,如果观察的对象有变化,就执行打印操作

const queueObserve = new Set();
//定义一个observe方法,用于将监听变化后的执行的操作添加到一个Set结构里
function observe(fn){
	queueObserve.add(fn)
}
//observable方法对监听对象的set方法进行拦截
function observable (obj){
	return new Proxy(obj,{
		set:function(target, name, value, receiver) {
		//在不修改原本操作的基础上
			const result = Reflect.set(target, name, value, receiver);
			// 执行所有添加的操作
			queueObserve.forEach(observer => observer());
			return result
		}
	})
}
// observable函数创建一个被观察的对象
const person = observable({
  name: '张三',
  age: 20
});

function print() {
  console.log(`${person.name}, ${person.age}`)
}
// observe添加对象变化后的打印方法
observe(print);
person.name = '李四';
// 李四,20


你可能感兴趣的:(es6,javascript,前端)