ES6标准入门(第3版)-笔记

准备工作

  1. 新建空工作文件夹
  2. 在该文件夹打开终端,并初始化项目,-y取默认配置,安装完毕出现package.json配置文件
    自动生成 package.json
$ npm init -y
  1. 后续可能需安装大量安装包,切换成淘宝镜像进行下载更快捷
#查看当前镜像源
$ npm get registry
#切换淘宝镜像,直接更换地址法
$ npm config set registry http://registry.npm.taobao.org/

1.6 Babel转码器

  1. 配置文件 .babelrc

格式:

{   
	"presets": [], 
	"plugins": [] 
}

presets字段设定转码规则,官方提供以下的规则集,按需安装

# 最新转码规则 
$ npm install --save-dev babel-preset-latest 
 
# react 转码规则 
$ npm install --save-dev babel-preset-react 
 
# 不同阶段语法提案的转码规则(共有4个阶段),选装一个 
$ npm install --save-dev babel-preset-stage-0 
$ npm install --save-dev babel-preset-stage-1 
$ npm install --save-dev babel-preset-stage-2 
$ npm install --save-dev babel-preset-stage-3 

配置规则:

{     
	"presets": [      
	"latest",       
	"react",       
	"stage-2"     
	],    
	"plugins": []   
} 

注:后续babel相关工具和模块的使用,.babelrc配置好是前提

  1. 命令行转码babel-cli

安装命令:

#全局安装
$ npm install --global babel-cli

# 安装在项目中
$ npm install --save-dev babel-cli 

基本用法:

#转码结果输出到标准输出 
$ babel example.js 
 
#转码结果写入一个文件 
# --out-file 或 -o 参数指定输出文件 
$ babel example.js --out-file compiled.js 
# 或者 
$ babel example.js -o compiled.js 
 
#整个目录转码 
# --out-dir 或 -d 参数指定输出目录 
$ babel src --out-dir lib 
#或者 
$ babel src -d lib 
 
# -s 参数生成source map文件 
$ babel src -d lib -s 

安装在项目中是改写package.json

{   
	"devDependencies": {     
		"babel-cli": "^6.0.0"   
	},   
	"scripts": {
		"build": "babel src -d lib"   
	}, 
}

转码时执行

$ npm run build
  1. babel-node
    babel-cli自带babel-node命令,提供REPL环境(可直接运行es6),执行"babel-node"直接进入REPL环境,或直接运行es6脚本——“babel-node es6.js”
    babel-cli安装在项目中时,package.json可配置如下:
{   
	"scripts": {     
		"script-name": "babel-node es6.js"   
	} 
}

"使用babel-node替代node,这样script.js本身就不用做任何转码处理" ——

  • node?不是babel?
  1. babel-register
    实时转码,适合开发环境 。当使用require加载 .js .jsx .es .es6 等文件时,该工具会将其自动转码,但不包括当前文件的转码

下载:

$ npm install --save-dev babel-register

使用:

require("babel-register");
require("./index.js");
  1. babel-core
    提供转码API

安装命令:

$ npm install babel-core --save 

使用:

var babel = require('babel-core'); 
 
// 字符串转码 
babel.transform('code();', options); // => { code, map, ast } 
 
// 文件转码(异步) 
babel.transformFile('filename.js', options, function(err, result) {   result; // => { code, map, ast } }); 
 
// 文件转码(同步) 
babel.transformFileSync('filename.js', options); // => { code, map, ast } 
 
// Babel AST转码 
babel.transformFromAst(ast, code, options); // => { code, map, ast } 

举例:

var es6Code = 'let x = n => n + 1'; 
var es5Code = require('babel-core')   
	.transform(es6Code, {     
	presets: ['latest']   
	})   
	.code; 
// '"use strict";\n\nvar x = function x(n) {\n  return n + 1;\n};' 
  1. babel-polyfill
    babel默认转换新的js语法,不转换新的API及一些定义在全局对象上的方法,使用babel-polyfill可对这些API和方法进行转码

安装:

$ npm install --save babel-polyfill 

使用:

import 'babel-polyfill'; 
// 或者 
require('babel-polyfill'); 

注:Babel 默认不转码的 API 非常多,详细清单可以查看babel-plugin-transform-runtime模块的definitions.js文件。

1.7 Traceur转码器

Traceur 允许将 ES6 代码直接插入网页
使用:

<script src="https://google.github.io/traceur-compiler/bin/traceur.js"></script> 
<script src="https://google.github.io/traceur-compiler/bin/BrowserSystem.js"></script> 
<script src="https://google.github.io/traceur-compiler/src/bootstrap.js"></script> 
<script type="module">   
	import './Greeter.js'; 
</script>

2.1 let

  1. 无变量提升,凡在let声明前使用,则报ReferenceError 错误 (var在声明前使用,值为undefined)
  2. 暂时性死区(TDZ) 在块级作用域中存在let命令,该变量就会绑定该区域,不受外部影响
var tmp=1;
if (true) {   
	// TDZ开始   
	tmp = 'abc'; // ReferenceError   
	console.log(tmp); // ReferenceError 
 
	let tmp; // TDZ结束   
	console.log(tmp); // undefined 
	
	tmp = 123;   
	console.log(tmp); // 123 
}
暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
  1. 不允许重复声明

2.2 块级作用域

  1. 函数声明与块级作用域
    ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。但浏览器为兼容旧代码,不会报错
    ES6 规定:(仅在浏览器的ES6环境中有效,其他环境不用遵守,即将块级作用域的函数声明当作let处理)

     a. 块级作用域中,函数声明语句的行为类似 let,在块级作用域之外不可引用
     b. 允许在块级作用域内声明函数(包含在大括号 {} 中才成立)
     c. 函数声明会提升到所在作用域的头部,,类似 var
    

考虑到环境导致的行为差异太大,当需要在块级作用域中声明函数时,应采用函数表达式的形式,而不是函数声明语句

// 函数声明语句 
{   
	let a = 'secret';   
	function f() {     
		return a;   
	} 
} 
 
// 函数表达式 
{   
	let a = 'secret';   
	let f = function () {     
		return a;   
	}; 
}
  1. const 本质
    const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合型数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指针,const只能保证这个指针是固定的。

    当需要将对象及其属性都变为常量(不可修改),应使用 Object.freeze 方法

var constantize = (obj) => {   
	Object.freeze(obj);   
	Object.keys(obj).forEach( (key, i) => {     
		if ( typeof obj[key] === 'object' ) {       
			constantize( obj[key] );     
		}   
	}); 
}; 

2.4 顶层对象的获取

ES5的顶层对象在各种实现里面不统一:

顶层对象 window self global
浏览器 X
Node X X
web Worker X X

为了取到顶层对象,现在一般使用 this 变量,但有局限性

  • 全局环境中,this会返回顶层对象。但是,Node 模块和 ES6 模块中,this返回的是当前模块。
  • 函数里面的this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this会指向顶层对象。但是,严格模式下,这时this会返回 undefined。
  • 不管是严格模式,还是普通模式,new Function(‘return this’)(),总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全政策),那么eval、new
    Function这些方法都可能无法使用。

以下两种方式勉强可以使用获取顶层对象:

// 方法一 
(typeof window !== 'undefined'    
	? window : (typeof process === 'object' &&       
		typeof require === 'function' &&       
		typeof global === 'object')      
		? global : this
); 
 
// 方法二 
var getGlobal = function () {   
	if (typeof self !== 'undefined') { return self; }   
	if (typeof window !== 'undefined') { return window; }   
	if (typeof global !== 'undefined') { return global; }   
	throw new Error('unable to locate global object'); 
}; 

垫片库 system.global 模拟了一个提案,可以在所有环境拿到 global。

// CommonJS 的写法 
var global = require('system.global')(); 
 
// ES6 模块的写法 
import getGlobal from 'system.global'; 
const global = getGlobal(); 

3.1 解构赋值

  1. 数组的解构赋值,以元素的次序取值
  2. 对象的解构赋值,以元素的名称取值(可重新取名)
//注:
//p:匹配模式,
//name:真正赋值的变量
let { p : name}=obj;

const node = {   
loc: {     
start: {       
line: 1,       
column: 5     }   } }; 
 
let { loc, loc: { start }, loc: { start: { line }} } = node; line // 1 loc  // Object {start: Object} start // Object {line: 1, column: 5} 
对象的解构赋值可以设置默认值,但生效条件是当对象的属性值严格等于 undefined
var {x = 3} = {x: undefined}; x // 3 
 
var {x = 3} = {x: null}; x // null 

解构赋值的规则是,只要等号右边的值不是对象或者数组,就先将其转为对象。由于 undefined 和 null 无法转为对象,所以对它们解构赋值会报错

let { prop: x } = undefined; // TypeError 
let { prop: y } = null; // TypeError 

变量声明语句不得使用圆括号——会引发歧义导致报错
赋值语句的非模式部分可以使用括号

4.2 codePointAt()

JavaScript 内部,字符以 UTF-16 的格式储存。\u0000~\uFFFF 之间的字符只表示 2 个字节的字符,超出 0xFFFF 的字符需要用 4 个字节表示
codePointAt 方法是测试一个字符由 2 个字节还是由 4 个字节组成的简单方法。其返回的是十进制码点,如需十六进制的值可用 toString 方法转换

let s = 'a'; 
//for...of 可以正确识别 32 位的 UTF-16 字符,即识别大于0xFFFF的码点
for (let ch of s) {   
	console.log(ch.codePointAt(0).toString(16)); 
} 
// 20bb7 // 61 

function is32Bit(c) {   
	return c.codePointAt(0) > 0xFFFF; 
}  
is32Bit("") // true 
is32Bit("a") // false 

与 codePointAt 相反的操作是:将码点返回对应的字符,可用 String.fromCodePoin 方法

注:fromCodePoint方法定义在String对象上,而codePointAt方法定义在字符串的实例对象上。

4.7 includes(), startsWith(), endsWith(),repeat(),padStart(),padEnd()

  • includes():返回布尔值,表示是否找到了参数字符串。
  • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
  • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
    第一个参数表示要查找的字符,第二个可选参数表示开始搜索的位置
let s = 'Hello world!'; 
 
s.startsWith('world', 6) // true 
s.endsWith('Hello', 5) // 
true s.includes('Hello', 6) // false 
  • repeat方法返回一个新字符串,表示将原字符串重复n次
  • padStart()用于头部补全
  • padEnd()用于尾部补全
//参数表示重复n次
'x'.repeat(3) // "xxx" 

//第一个参数表示总字符串长度
//第二个可选参数表示需要补全的字符,无则补空
'x'.padStart(5, 'ab') // 'ababx' 
'x'.padEnd(5, 'ab') // 'xabab' 
'x'.padStart(4) // '   x'
'123456'.padStart(10, '0') // "0000123456" 

5.2 字符串的正则方法

字符串对象的以下4个方法,在语言内部全部调用RegExp的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp对象上。

String.prototype.match 调用 RegExp.prototype[Symbol.match]
String.prototype.replace 调用 RegExp.prototype[Symbol.replace]
String.prototype.search 调用 RegExp.prototype[Symbol.search]
String.prototype.split 调用 RegExp.prototype[Symbol.split]

5.3 u 修饰符

ES6添加了 u 修饰符,含义为“Unicode 模式”,能正确处理大于 \uFFFF 的 Unicode 字符,即4字节的 UTF-16 编码
当使用 u 字符后,以下正则修饰符的行为就会改变

  1. 点字符
    点(.)字符在正则中表示除行终止符( \n | \r | 行分隔符 | 段分隔符 )以外的任意单个字符,加上 u 修饰符后才能识别码点大于 0xFFFF 的Unicode字符
var s = ''; 
 
/^.$/.test(s) // false 
/^.$/u.test(s) // true 
注:若要点字符匹配任意字符,可加上 “s” 修饰符
  1. Unicode 字符
    ES6 新增了使用大括号表示 Unicode 字符,这种表示法在正则表达式中必须加上u修饰符,才能识别当中的大括号,否则会被解读为量词
/\u{61}/.test('a') // false 
/\u{61}/u.test('a') // true 
/\u{20BB7}/u.test('') // true 
  1. y 修饰符
    y 修饰符隐式包含了 ”^“ 修饰符,即让头部匹配在全局匹配中都有效。当匹配一个后,会从剩余的字符串开头重新匹配(“g” 修饰符不包含从“头”匹配之意)
    检测是否设置了 y 修饰符用 sticky 属性

5.8 后行断言

”先行断言“指的是,x只有在y前面才匹配,必须写成 /x(?=y)/
”先行否定断言“指的是,x只有不在y前面才匹配,必须写成 /x(?!y)/
“后行断言”,x只有在y后面才匹配,必须写成 /(?<=y)x/
”后行否定断言“,x只有不在y后面才匹配,必须写成 /(?

返回结果均不包含括号里的内容。采用先从左到右扫描,发现匹 配以后再回过头,即“先右后左”的执行顺序

/\d+(?=%)/.exec('100% of US presidents have been male')  // ["100"] 
/\d+(?!%)/.exec('that’s all 44 of them')                 // ["44"]
/(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill')  // ["100"] 
/(?<!\$)\d+/.exec('it’s is worth about €90')                // ["90"]

后行断言的组匹配

//由于执行顺序是从右到左,第二个括号是贪婪模式,第一个括号只能捕获一个字符,所以结果是1和053
/(?<=(\d+)(\d+))$/.exec('1053') // ["", "1", "053"] 

//第一个括号是贪婪模式,第二个括号只能捕获一个字符,所以结果是105和3
/^(\d+)(\d+)$/.exec('1053') // ["1053", "105", "3"] 

6. 数值扩展

6.2 Number.isFinite(), Number.isNaN()
检测数值是否有限和是否非数字

6.3 Number.parseInt(), Number.parseFloat()

6.4 Number.isInteger()
判断值是否为整数

6.5 Number.EPSILON
是JS能够表示的最小精度,其目的在于为浮点数计算,设置一个误差范围

function withinErrorMargin (left, right) {   
	return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2); 
} 
 
0.1 + 0.2 === 0.3 // false 
withinErrorMargin(0.1 + 0.2, 0.3) // true 
 
1.1 + 1.3 === 2.4 // false 
withinErrorMargin(1.1 + 1.3, 2.4) // true 

6.7 Math 对象的扩展

  1. Math.trunc() 去除小数部分,返回整数部分。
    手动实现:
Math.trunc = Math.trunc || function(x) {   
return x < 0 ? Math.ceil(x) : Math.floor(x); 
};
  1. Math.sign() 判断一个数到底是正数、负数、还是零
    返回值:

     参数为正数,返回+1; 
     参数为负数,返回-1; 
     参数为 0,返回0;
     参数为-0,返回-0; 
     其他值,返回NaN。
    

    手动实现:

Math.sign = Math.sign || function(x) {   
	x = +x; // convert to a number   
	if (x === 0 || isNaN(x)) {     
		return x;   
	}   
	return x > 0 ? 1 : -1; 
}; 

7. 函数扩展

  1. 默认参数
    一般至于尾部,传参时可省。置于非尾部时,不可省,可显式传入"undefined",触发默认值(“null” 无法触发默认值)
    设置默认属性后,函数的 length 属性不会将设有默认值的参数及其后的参数计入总数

  2. rest 参数
    形式为:…变量名,是一个数组(arguments对象是类数组)rest参数只能置于末尾

  3. 箭头函数
    箭头函数使用注意点:

     a. 函数体内的 this 对象,就是【定义】时所在的对象,而不是使用时所在的对象
     b. 不可当作构造函数,即不可以使用 new 命令,否则抛出错误
     c. 不可使用 arguments 对象,该对象在函数体内不存在。如需使用,可用 rest 参数代替
     d. 不可使用 yield 命令, 即不能用作 Generator 函数
    
function foo() {   
	setTimeout(() => {     
		console.log('id:', this.id);   
	}, 100); 
} 
 
var id = 21;  
foo.call({ id: 42 }); // id: 42 
//foo函数生成时,setTimeout 的 this 就被指定了。setTimeout 等到执行时 this 不会像普通函数一样指向 window
  1. 双冒号运算符
    形如: 对象 :: 函数 ——将左边的对象作为上下文环境(this 对象) 绑定到右边的函数上面
foo::bar; 
// 等同于 
bar.bind(foo); 
 
foo::bar(...arguments); 
// 等同于 
bar.apply(foo, arguments); 
  1. 尾调用优化
    尾调用:某个函数的最后一步操作是调用另一个函数(形如 : return g(x))
    注:只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则无法进行“尾调用优化”
//尾调用优化例子:
function f() {   let m = 1;   let n = 2;   return g(m + n); } f(); 
 
// 等同于 
function f() {   return g(3); } f(); 
 
// 等同于 
g(3); 

//尾调用不能优化例子:
function addOne(a){   
	var one = 1;   
	function inner(b){     
		return b + one;   
	}   
	return inner(a); 
}
//调用inner时只传了b,内部需要计算的one,还需向父级获取,所以不能删除父级的调用帧,无法进行优化
  1. 尾递归
    尾递归即尾调用自身。ES6明确规定,所有ECMAScript 的实现都必须部署“尾递归优化”
    因为需要保存成百上千个调用栈,递归非常消耗内存,容易发生“栈溢出”错误
    但尾递归只存在一个调用栈,所以永远不会发生“栈溢出”错误
    Fibonacci 数列举例:
function Fibonacci (n) {   
	if ( n <= 1 ) {
		return 1
	};  
	return Fibonacci(n - 1) + Fibonacci(n - 2); 
}  
Fibonacci(10) // 89 
Fibonacci(100) // 堆栈溢出 
Fibonacci(500) // 堆栈溢出

//尾递归优化:
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {   
	if( n <= 1 ) {
		return ac2
	};  
	return Fibonacci2 (n - 1, ac2, ac1 + ac2); 
}  
Fibonacci2(100) // 573147844013817200000 
Fibonacci2(1000) // 7.0330367711422765e+208 
Fibonacci2(10000) // Infinity 

尾调用优化仅在严格模式下开启,因为正常模式下函数内部有两个变量可以跟踪函数的调用栈,严格模式下会改写调用栈,这两个变量会失真

	func.arguments:返回调用时函数的参数。
	func.caller:返回调用当前函数的那个函数

正常模式下,需要手动改写尾递归为“循环”,这样减少调用栈就不会造成溢出了

8. 数组扩展

8.1 扩展运算符
扩展运算符用于数组赋值时只能放在参数的最后一位

//当一个函数需要接受单个参数,但调用该方法时传入的参数是数组时,可以用扩展运算符快捷传入
//普通函数
function f(x, y, z) {   // ... } 
var args = [0, 1, 2]; 
// ES5 的写法 
f.apply(null, args);  
// ES6的写法 
f(...args);

//数组插入数据
var arr1 = [0, 1, 2]; 
var arr2 = [3, 4, 5]; 
//ES5
Array.prototype.push.apply(arr1, arr2); 
//ES6
arr1.push(...arr2); 

//复制数组
//数组时复合数据类型,简单复制时只能复制指针指向,原数组和新数组指向的是同一份数据
const a1 = [1, 2]; 
//以下是拷贝新数组的方式:
//ES5:变通方式
const a2 = a1.concat(); 
a2[0] = 2; // a1 = [1, 2] 
//ES6
const a2 = [...a1]; // 或者 
const [...a2] = a1; 
扩展运算符能将字符串转为数组,且能正确识别 4 字节的 Unicode 字符
凡有 Iterator 接口的对象都能用扩展运算符转为数组

8.2 Array.from()
能将类似数组的对象(array-like object,必有 length 属性)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)转为真正的数组,类数组对象常见的有函数内部的 arguments 对象,DOM操作返回的 NodeList 集合

注:1. 扩展运算符底层调用的是 Symbol.iterator ,所以转换的对象必须要部署这个接口
	2. Array.from 能转换类数组对象,只要转换对象里包含 length 属性就能被转换真正的数组

8.3 Array.of
Array.of 用于将一组值转换为数组。基本上可以用来替代Array()或new Array(),并且不存在由于参数不同而导致的重载。
方法模拟:

function ArrayOf(){   
	return [].slice.call(arguments); 
} 

8.4 copyWithin()

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

// 对于没有部署 TypedArray 的 copyWithin 方法的平台 需要采用下面的写法 
[].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4); 
// Int32Array [4, 2, 3, 4, 5] 

8.5 find() 和 findIndex()
此方法用于找出第一个符合条件的数组成员或成员所在的位置。
其参数是一个回调函数,所有数组成员依次执行该回调函数,找不到符合条件的成员,返回 undefined或 “-1
其参数有3个,依次为当前值、当前位置、原数组
这两个方法可以弥补 indexof 方法的不足,能找出为 “NaN” 的成员( indexof 方法内部进行严格相等运算符“===”进行运算,导致对 NaN 误判)

[1, 5, 10, 15].findIndex(function(value, index, arr) {   
	return value > 9; 
}) // 2

[NaN].indexOf(NaN) // -1 
 
[NaN].findIndex(y => Object.is(NaN, y)) // 0 

8.6 fill()

8.7 entries(),keys() 和 values()

8.8 includes()
Array.prototype.includes 方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似
MapSet 数据结构有一个has方法,需要注意与includes区分
—— Map 结构的has方法,是用来查找键名
—— Set 结构的has方法,是用来查找

8.9 数组的空位
空位不是 undefined ,空位表示没有任何值

Array(3) // [, , ,]   数组空位

0 in [undefined, undefined, undefined] // true 
0 in [, , ,] // false 

ES5 对空位的处理:

  • forEach(), filter(), every() 和 some() 都会跳过空位。
  • map() 会跳过空位,但会保留这个值
  • join() 和 toString() 会将空位视为 undefined,而 undefinednull 会被处理成空字符串。

ES6明确规定将空位转为 undefined

9. 对象扩展

9.4 Object.is()
ES5比较两个值是否相等都有缺点:
==:会动态转换数据类型
===:NaN不等于自身,+0 = -0
ES6部署的新方法 Object.is() 行为与严格比较运算符一致,但修复了上述NaN和正负零的问题

9.5 Object.assign()
此方法用于对象的合并,将源对象的所有可枚举属性(及自带属性(非继承属性))复制到目标对象
格式形如:Object.assign(target, source1, source2);

如果目标参数不是对象,该方法会默认将其转换为对象再合并(由于  undefined 和  null  无法转为对象,所以会报错)
源参数为 undefined 或 null 、布尔值、Number 时,会直接忽略

注意点:

  • Object.assign 方法是浅拷贝
  • 同名属性后者替换前者,不是追加
  • 复制数组时,将数组视为对象按照下标位(index)进行替换、合并处理
  • 源参数是取值函数时,直接复制函数执行后的值

常见应用:
为属性指定默认值

const DEFAULTS = {   logLevel: 0,   outputFormat: 'html' }; 
 
function processContent(options) {   
	options = Object.assign({}, DEFAULTS, options);  //DEFAULTS, options最好都是简单类型,不要指向另一个对象(浅拷贝)
	console.log(options);   // ... 
}

9.6 可枚举和遍历
目前,有四个操作会忽略enumerable为false的属性。

  • for…in循环:只遍历对象自身的和继承的可枚举的属性。(不推荐使用)
  • Object.keys():返回对象自身的所有可枚举的属性的键名数组。(推荐使用)
  • JSON.stringify():只串行化对象自身的可枚举的属性。
  • Object.assign(): 忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。

ES6 一共有 5 种方法遍历对象的属性

  • for…in
  • Object.keys(obj)
  • Object.getOwnPropertyNames(obj) :返回对象自身的(包含不可枚举,不包含Symbol属性)键名数组
  • Object.getOwnPropertySymbols(obj):返回对象自身的所有Symbol属性的键名数组
  • Reflect.ownKeys(obj):返回对象自身所有键名数组(不管键名是 Symbol 或字符串,也不管是否可枚举)

以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。

  • 首先遍历所有数值键,按照数值升序排列。
  • 其次遍历所有字符串键,按照加入时间升序排列。
  • 后遍历所有 Symbol 键,按照加入时间升序排列。

9.7. Object.getOwnPropertyDescriptors()
9.8. __proto__属性,Object.setPrototypeOf(),Object.getPrototypeOf()

9.9 super
super 关键字表示原型对象时,只能用在对象的方法中,用在其他地方会报错

const proto = {   foo: 'hello' };  
//只有这种对象方法的简写法能被JS确定
const obj = {   
	find() {     
		return super.foo;   
	} 
};  
Object.setPrototypeOf(obj, proto); 
obj.find() // "hello"

9.10 Object.keys(),Object.values(),Object.entries()
三者分别返回对象自身(不包含继承)的、可遍历属性的键名、键值、键值对数组,作为遍历对象的补充手段,供 for…of循环使用

Object.values()
返回键值数组是以键名排列的顺序为主,会过滤键名为 Symbol 值的属性
参数是字符串时返回各个字符组成的数组
参数非对象时,先将其转为对象。数值和布尔值的包装对象不会为实例添加非继承属性,所以会返回 空数组

9.11 对象的扩展运算符

  1. 解构赋值
    解构赋值等号右边须是对象,undefined 和 null 会报错
    解构赋值必须是最后一个参数
    解构赋值是浅拷贝,不能复制继承自原型对象的属性

  2. 扩展运算符
    扩展运算符(…)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。等同于 Object.assign ,仅拷贝对象实例的属性
    要克隆完整的对象,还要拷贝对象原型上的属性:

// 写法一 
const clone1 = {   
	__proto__: Object.getPrototypeOf(obj),   //__proto__属性在非浏览器环境不一定部署,不推荐使用
	...obj 
}; 
 
// 写法二 
const clone2 = Object.assign(   
	Object.create(Object.getPrototypeOf(obj)),   
	obj 
); 
 
// 写法三 
const clone3 = Object.create(   
	Object.getPrototypeOf(obj),   
	Object.getOwnPropertyDescriptors(obj) 
) 
扩展运算符	放在自定义属性后面,扩展运算符里的同名属性会被覆盖,放在自定义属性前面,常用来设置新对象的默认属性值

9.12 Null 传导运算符
层层判断对象是否存在,写法:?.

//读取message.body.user.firstName
const firstName = (message   
&& message.body   
&& message.body.user   
&& message.body.user.firstName) || 'default'; 

//新提案写法
const firstName = message?.body?.user?.firstName || 'default'; 

10 Symbol

生成的 Symbol 是一个原始类型的值,不是对象,不能使用 new 命令。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型

  • Symbol 函数的参数只是表示对当前 Symbol 值的描述
  • Symbol 值不能与其他类型的值进行运算,会报错
  • Symbol 值可以显式转为字符串
  • Symbol 值也可以转为布尔值,但是不能转为数值

10.2 作为属性名
设置和获取时都须用 “[ ]” 包裹,不能使用 “.”运算符
Symbol 值作为属性名时,该属性还是公开属性,不是私有属性

10.4 属性名的遍历
Symbol 作为属性名,该属性不会出现在for…in、for…of循环中,也不会被Object.keys()Object.getOwnPropertyNames()、 JSON.stringify() 返回。但是,它也不是私有属性,有一个Object.getOwnPropertySymbols方法,可以获取指定对象的所有 Symbol 属性名。 (Reflect.ownKeys(obj) 也可)

注:
Object.defineProperty(obj, prop, descriptor)   
参数说明:要定义属性的对象、要定义或修改的属性的名称或 Symbol 、要定义或修改的属性描述符
是定义 key 为 Symbol 属性的方法之一

10.5 Symbol.for()
Symbol.for() 产生的 symbol 会被放置在全局中,下次 Symbol.for() 传参时,首先查看全局下是否有同样参数的 Symbol 无则返回新 Symbol ,有则返回原 Symbol 。即Symbol.for(‘A’) 产生的是同一个数据,而 Symbol(‘A’) 产生的是不同的数据

11 Set 和 Map 数据结构

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

//用法:
const s = new Set(); 
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x)); //添加数据
//或 s = new Set([2, 3, 5, 4, 5, 2, 2])  可接受具有 'iterable' 接口的数据结构作为参数,用来初始化 
[...s];//[2, 3, 5, 4]
s.size //4
Set 进行值判断的算法同 精准相等运算符(===),但该运算 NaN 不等于自身,Set 结构中的 NaN 等于自身

Set实例:

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

  2. 方法

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

    Array.from 方法可以将 Set 结构转为数组。所以去除数组重复数据可以: Array.from(new Set(array))

    遍历方法:
    keys():返回键名的遍历器
    values():返回键值的遍历器 ,可用 for…of 替代循环遍历(Set结构默认遍历器生成函数就是 values 方法)
    entries():返回键值对的遍历器
    forEach():使用回调函数遍历每个成员

    Set 遍历顺序即插入顺序,Set 结构没有键名(或键名和键值是同一个值), keys() 和 value() 方法行为一致
    Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference)

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} 
 
// 差集 
let difference = new Set([...a].filter(x => !b.has(x))); // Set {1}

11.2 WeakSet
与 Set 的区别:
WeakSet 的成员只能是对象;
WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用。垃圾回收机制运行不可预测,运行前后 WeakSet 成员数量不定,所以 ES6 规定 WeakSet 不可遍历(WeakMap 亦同)

方法:(没有 size 、forEach 属性)

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

用处:可用于储存 DOM 节点,而不用担心这些节点从文档移除时,会引发内存泄漏

11.3 Map
类似对象,是 “值-值” 键值对的集合,键名不限定是字符串(Object 结构提供的是 “字符串—值” 的键值对)
使用:

const m = new Map(); 
const o = {p: 'Hello World'}; 
 
m.set(o, 'content') //键名为对象
m.get(o) // "content" 
 
m.has(o) // true 
m.delete(o) // true 
m.has(o) // false 

const map = new Map([   //数组作参数,每个数组成员是表示键值对的数组
['name', '张三'],   
['title', 'Author'] 
]); 
 
map.size // 2 
map.has('name') // true 
map.get('name') // "张三" 

注:
Map 的键实际上是跟内存地址绑定的,只要内存地址不一样就视为两个键
当键是简单类型时,只要两个值严格相等就是一个键。如 0 严格等于 -0 ,而 true 不严格等于 ‘true’undefined 不严格等于 null
例外:NaN 不严格等于自身,但 Map 将其视为同一个键

属性和方法:

  1. size 返回成员总数
  2. set(key,value) 设置键值对,键名可被覆盖,返回当前的 Map 对象,可采用链式写法
  3. get(key) 获取对应键名的键值,找不到则返回 undefined
  4. has(key) 判断键名是否在当前Map中,返回布尔值
  5. delete(key) 删除指定键,返回布尔值
  6. clear() 清除所有成员,无返回值

遍历方法:keys() 、value() 、entries() (默认遍历器接口) 、forEach()
Map结构可使用扩展运算符(…)快速转换为数组结构,这样能使用到数组自带的多种方法,如map、filter
forEach() 可接受第二个参数,用来绑定 this

const reporter = {   
	report: function(key, value) {     
		console.log("Key: %s, Value: %s", key, value);   
	} 
}; 
 
map.forEach(function(value, key, map) {   
	this.report(key, value); 	//该this指向 reporter
}, reporter);

Map 与其他数据结构互转

  1. Map 与数组
  2. Map 与对象
  3. Map 与 JSON
    Map 转 JSON
    当Map 的键名是都是字符串时,可转为对象JSON
function strMapToObj(strMap) {   
	let obj = Object.create(null);   
	for (let [k,v] of strMap) {     
		obj[k] = v;   
	}   
	return obj; 
} 

function strMapToJson(strMap) {   
	return JSON.stringify(strMapToObj(strMap)); //Map => Obj => Json 
} 
 
let myMap = new Map()
	.set('yes', true)
	.set('no', false); 

strMapToJson(myMap) // '{"yes":true,"no":false}' 

当键名有非字符串时,可转为数组JSON

function mapToArrayJson(map) {   
	return JSON.stringify([...map]); //Map => array => json
} 
 
let myMap = new Map()
	.set(true, 7)
	.set({foo: 3}, ['abc']); 

mapToArrayJson(myMap) // '[[true,7],[{"foo":3},["abc"]]]' 

JSON 转 Map
正常情况下,所有键名都是字符串

function objToStrMap(obj) {   
	let strMap = new Map();   
	for (let k of Object.keys(obj)) {     
		strMap.set(k, obj[k]); 
	}   
	return strMap; 
} 
function jsonToStrMap(jsonStr) {   
	return objToStrMap(JSON.parse(jsonStr)); //json => obj => Map
} 
 
jsonToStrMap('{"yes": true, "no": false}') 
// Map {'yes' => true, 'no' => false}

特殊情况,整个JSON是个数组,且成员本身又是一个有两个成员的数组

function jsonToMap(jsonStr) {   
	return new Map(JSON.parse(jsonStr)); //
} 
 
jsonToMap('[[true,7],[{"foo":3},["abc"]]]') // Map {true => 7, Object {foo: 3} => ['abc']} 

11.4 WeakMap
只接受对象作为键名(null 除外);键名所指的对象不计入垃圾回收机制,防止内存泄露
典型的应用场景是在网页上的DOM元素上添加数据,使用WeakMap结构后,当DOM元素被清除,其所对应的WeakMap记录就会自动移除
方法:get()、set()、 has()、delete()
无遍历方法
调试:如过引用所指向的值占用特别多的内存,可以通过 Nodeprocess.memoryUsage方法可以看出引用是否解除
操作关键步骤:

#--expose-gc 表示允许手动执行垃圾回收机制
$ node --expose-gc
#手动执行垃圾回收
global.gc();
#外部引用一次
let arr=new Array(5*1024*1024)
#弱引用一次
let wm = new WeapMap();
wm.set(arr,1)
#在清空arr前后(执行垃圾回收)可查看内存大小变化情况

12 Proxy

Proxy :代理。当访问目标对象时,必须先通过 “代理层”,该代理层可以对外界的访问进行过滤和改写
ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。

var proxy = new Proxy(target, handler); 
//target:要拦截的目标对象
//handler:定制拦截行为

Proxy 支持的拦截操作:

  • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy[‘foo’]。参数:目标对象(被拦截对象)、属性名、proxy实例本身(可选)
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy[‘foo’] = v,返回一个布尔值。参数:目标对象、属性名、属性值、Proxy实例本身(可选)
  • has(target, propKey):拦截propKey in proxy的操作,即判断对象是否具有某个属性,返回一个布尔值。 (对 for…in 不生效,对 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.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额 外操作可以拦截。
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(…args)、proxy.call(object, …args)、 proxy.apply(…)。 参数:目标对象、目标对象的上下文对象(this)、目标对象的参数数组
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作(拦截 new 操作),比如new proxy(…args)。参数:目标对象、构建函数的参数对象

this 指向问题
目标对象的内部 this 会指向 Proxy 代理,以目标对象直接访问是不经拦截的

13 Reflect

不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。

let obj={name:'jack',age:18}

//拦截obj对象,并修改其默认方法:
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);   
	} 
}); 

//运行,使用代理对象操作属性
Reflect.get(loggedObj,'name')
//get {name: "jack", age: 18} name
//"jack"

Reflect.deleteProperty(loggedObj,'name')
//deletename
//true

Reflect 有13个静态方法,与 Proxy 对象一一对应:

  • Reflect.apply(target, thisArg, args) 参数:目标参数、目标函数调用时绑定的this对象、类数组实参
  • Reflect.construct(target, args)
  • Reflect.get(target, name, receiver) 参数:目标对象、目标对象的属性、给定的上下文对象(name部署getter时,其中的this指向receiver)
  • Reflect.set(target, name, value, receiver) name属性设置了赋值函数(setter)时,赋值函数的this绑定receiver
  • Reflect.defineProperty(target, name, desc) 为对象定义属性时请使用该方法,Object.defineProperty 将废除 参数:目标对象、属性名、属性描述
  • Reflect.deleteProperty(target, name)
  • Reflect.has(target, name)
  • Reflect.ownKeys(target)
  • Reflect.isExtensible(target) 检查对象是否可扩展
  • Reflect.preventExtensions(target) 阻止对象扩展
  • Reflect.getOwnPropertyDescriptor(target, name) 第一个参数非对象会报错
  • Reflect.getPrototypeOf(target) 参数不是对象会报错,Object.getPrototypeOf不会(会提前将参数转为对象)
  • Reflect.setPrototypeOf(target, prototype)第一个参数不是对象会报错( undefined 或 null 同)

Reflect.set:
如果 Proxy 对象和 Reflect 对象联合使用,前者拦截赋值操作,后者完成赋值的默认行为,而且传入了receiver,那么Reflect.set会触发 Proxy.defineProperty拦截

let p = {   a: 'a' }; 
 
let handler = {   
	set(target, key, value, receiver) {     
		console.log('set');     
		Reflect.set(target, key, value, receiver)   
	},   
	defineProperty(target, key, attribute) {     
		console.log('defineProperty');     
		Reflect.defineProperty(target, key, attribute);   
	} 
}; 
 
let obj = new Proxy(p, handler); 
obj.a = 'A'; 
// set 
// defineProperty 

//上面代码中,Proxy.set拦截里面使用了Reflect.set,而且传入了receiver,导致触发Proxy.defineProperty拦截。
//这是因为Proxy.set的 receiver参数总是指向当前的 Proxy 实例(即上例的obj),
//而Reflect.set一旦传入receiver,就会将属性赋值到receiver上面(即obj),导致触发defineProperty拦截。
//如果Reflect.set没有传入receiver,那么就不会触发defineProperty拦截。

使用 Proxy 实现观察者模式
观察者模式(Observer mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行。

const queuedObservers = new Set(); //观察者集合
 
const observe = fn => queuedObservers.add(fn); 
const observable = obj => new Proxy(obj, {set}); //返回原始对象的 Proxy ,拦截赋值操作
 
function set(target, key, value, receiver) {   
	const result = Reflect.set(target, key, value, receiver);   //赋值操作
	queuedObservers.forEach(observer => observer());   //触发每一个观察者都执行一遍
	return result; 
} 

const person = observable({   //被观察者
	name: '张三',   
	age: 20 
}); 
 
function print() {   //观察者
	console.log(`${person.name}, ${person.age}`) 
} 
 
observe(print); 
person.name = '李四'; 
// 输出 // 李四, 20

14 Promise 对象

Promise 是异步编程的一种解决方案,原生提供了 Promise 对象。简单来说就是一个容器,保存着某个未来才会结束的事件的结果
特点: Promise 对象的状态:pending(进行中)、fulfill(已成功)、rejected(已失败),只有异步操作的结果可以将 pending 改为 fulfill 或者rejected,一旦改变就不再变化
如果某些事件不断地反复发生,一般来说,使用 Stream 模式是比部署 Promise 更好的选择。

14.3 Promise.prototype.then()
then 方法包含两个参数:(resolve(),reject())当前面的promise返回resolved状态即成功状态,则执行resolved方法,当返回rejected 状态即失败状态,则执行 rejected 方法
then 里面的参数方法返回的不新的 Promise 实例,所以可以采用链式写法

14.4 Promise.prototype.catch()

  1. catch 方法 等同于 .then(null,reject) 抛出错误
  2. 如果 Promise 状态已经变成 resolved ,再抛出错误是无效的,状态一旦改变即永久保持
  3. 错误会一直向后传递,直到被捕获
  4. 一般来说,不要在 then 方法里面定义 rejected 状态的回调函数 ,而在末端使用 catch 方法,这样可以处理promise 内部发生的错误
  5. 即使 promise 没有处理错误情况,也只会报错而不影响外层代码的执行

14.5 Promise.all()

  1. Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例
  2. 参数要求:不一定是数组,但必须具有 Iterator 接口,且每个成员都是 promise 实例(如果不是,会先调用 Promise.resolve 将参数转为实例)
  3. promise 的状态:
    当参数全部状态都是 fulfilled,那么 p 的状态也变成 fulfilled,参数的返回值组成数组传递给 p 的回调函数
    只要有一个参数 rejected ,p 的状态就变成 rejected ,第一个 reject 的参数返回值传递给 p 的回调函数
  4. 当参数有自己的 catch 方法时,不会调用 all 方法后的 catch 方法

14.6. Promise.race()
race 方法同 all 方法,只不过是当参数中有一个实例率先改变状态,那么 p 也跟着改变状态,该实例的返回值传递给 p 的回调函数

14.7. Promise.resolve()

  1. resolve 方法:将现有对象转为 Promise 对象
  2. 参数说明:
    若是一个 promise 实例,则直接返回这个实例
    若是一个 thenable 对象(即具有 then 方法的对象)会将该对象转为 promise 对象,然后立即执行该对象的 then 方法
    若是个原始值,或者不具有 then 方法的对象,返回一个新 promise 对象,状态为 resolved
    若无参,在本轮“事件循环”结束时,直接返回一个 resolved 状态的 promise 对象

14.8. Promise.reject()
reject 返回一个新的 promise 实例,状态为 rejected
该方法的参数,会作为 reject 的理由,变成后续方法的参数

14.9 附加方法
done():可以捕获最后一个方法抛出的错误

Promise.prototype.done = function (onFulfilled, onRejected) {   
	this.then(onFulfilled, onRejected)     
		.catch(function (reason) {       
		// 抛出一个全局错误       
		setTimeout(() => { throw reason }, 0);     
	}); 
};

finally():接受一个普通回调函数作为参数,不管 promise 的状态是什么都会执行改回调函数

Promise.prototype.finally = function (callback) {   
	let P = this.constructor;   
	return this.then(     
		value  => P.resolve(callback()).then(() => value),     
		reason => P.resolve(callback()).then(() => { throw reason })   
	); 
};

14.10 应用
预加载图片

const preloadImage = function (path) {   
	return new Promise(function (resolve, reject) {     
		const image = new Image();     
		image.onload  = resolve;     
		image.onerror = reject;     
		image.src = path;   
	}); 
};

14.11. Promise.try()

15. Iterator 遍历器和 for…of 循环

15.1 Iterator
Iterator 的作用
1. 为各种数据结构提供一个统一的渐变访问接口
2. 使得数据结构的成员能按次序排列
3. 主要供 for…of 使用

遍历过程:
遍历器本质上就是一个指针对象,第一次调用指针对象的 next 方法,指针指向数据结构的第一个成员,第二次指向第二个成员…每次调用 next 会返回一个包含 value(当前成员的值) 和 done(遍历是否结束) 两个属性的对象

16(17). Generator

18. async

18.1 async 与 generator 的区别

1. generator 函数必须依赖执行器执行,如 co 模块能直接将其执行或者调用 next 方法;而async 函数自带执行器,可像普通函数一样直接执行
2. async 语义更清晰,async 表示函数里有异步操作,await 表示紧跟后面的表达式需等待结果
3. async  函数的await 后面可以是Promise 对象和原始类型的值(此时相当于同步操作)
4. async 函数返回值是 Promise 对象,比 Generator 返回 Iterator 对象方便,可以用 then 方法指定接下来的操作
async 函数相当于是多个异步操作包装成 Promise 对象,await 就是内部 then 命令的语法糖
async 函数内部是继发执行,外部不受影响

19(20). Class

19.1
类的方法都是定义在 prototype 对象上,Object.assign 方法可以一次像类添加多个方法:

Object.assign(Point.prototype, {   
	toString(){},   
	toValue(){} 
});

与ES5相比,类内部定义的方法不可枚举,但ES5中可枚举

19.7 添加私有方法

  1. 在类外面添加方法,然后类内部调用
  2. 在类里面的私有方法名称命名为一个 Symbol 值
  3. 有提案使用 # 关键字表示私有,可以加在属性和方法前表示私有属性和私有方法

19.13 静态方法

  1. 在方法前加 static 关键字,表示私有。私有方法不能被实例继承,而是直接通过类本身来调用。静态方法中的 this 指的是类而不是实例。私有方法能被子类继承;子类的 super 关键字可以调用父类的静态方法
  2. 类内部只有静态方法,没有静态属性(只可在类外部定义:Foo.prop=1),只有实例属性(即通过 this 添加的属性)。(有提案提出了新的实力属性和静态属性写法)

19.15 new.target
该属性一般用于构造函数中,返回new 命令作用于的那个构造函数。如果构造函数不是通过 new 命令调用的会返回 undefined
注:子类继承父类时,new.target 会返回子类;在函数外部,使用该属性会报错
根据其特性,可写出不能独立使用、必须继承后才能使用的类

20.1 继承

  1. 子类的构造器中一定要调用 super ,因为子类没有自己的 this 对象,而是继承父类的 this 对象后进行个性化加工。如果不调用super ,子类就得不到 this 对象
  2. 子类能继承父类的静态方法
  3. Object.getPrototypeOf() 可用来判断一个类是否继承自另一个类
  4. super 既可当函数调用也可当作对象使用。当作为函数调用时代表父类的构造函数,返回的是子类的实例,且子类的构造函数必须执行一次 super(),且只能在构造函数中使用。当作为对象使用时,在普通方法中指向父类的原型(super==F.prototype),这时,子类无法通过调用super获取到父类实例上的方法或属性(this.XX)。当作为对象用在静态方法中时,super 指向父类而不是父类的原型
class Parent {   
	static myMethod(msg) {     
		console.log('static', msg);   
	} 
 
	 myMethod(msg) {     
	 	console.log('instance', msg);   
	 } 
 } 
 
class Child extends Parent {   
	static myMethod(msg) {     
		super.myMethod(msg);   
	} 
 
	myMethod(msg) {     
		super.myMethod(msg);   
	} 
} 
 
Child.myMethod(1); // static 1 
//此处是类自身调用私有方法,即 static myMethod,然后静态方法中的 super 指向的是父类本身的私有方法。
 
var child = new Child(); 
child.myMethod(2); // instance 2 
//此处是实例对象调用原型方法,即 myMethod,然后该普通方法中的 super 指向的是父类的原型对象。

20.4 类的 prototype 属性和 __proto__ 属性

  1. 子类的 __proto__ 属性表示构造函数的继承,总是指向父类
  2. 子类 prototype 属性的 __proto__ 属性表示方法的继承,总是指向父类的 prototype 属性
//继承的原理
class A { }  
class B { } 
 
// B 的实例继承 A 的实例 
Object.setPrototypeOf(B.prototype, A.prototype); 
 
// B 的实例继承 A 的静态属性 
Object.setPrototypeOf(B, A); 
 
const b = new B(); 

//setPrototypeOf 的原理
Object.setPrototypeOf = function (obj, proto) {   
	obj.__proto__ = proto;   
	return obj; 
}

//so...
class A { } 
 
class B extends A { } 
let a = new A();
let b = new B();
 
B.__proto__ === A // true 
B.prototype.__proto__ === A.prototype // true 
b.__proto__.__proto__ === a.__proto__  //true

总结:

  1. 子类(B)作为一个对象,B 的原型(__proto__ 属性)是父类(A)
  2. 子类(B)作为一个构造函数,B 的原型对象(prototype属性)是父类的原型对象(prototype属性)的实例

extends 的继承目标
class B extends A{} A只要是有 prototype 属性的函数就行,即除了Function.prototype函数,A可以是任意函数

//情况一
class A extends Object { }  
A.__proto__ === Object // true 
A.prototype.__proto__ === Object.prototype // true 
//这种情况下,A其实就是构造函数Object的复制,A的实例就是Object的实例。

//情况二
class A { }  
A.__proto__ === Function.prototype // true 
A.prototype.__proto__ === Object.prototype // true 
//这种情况下,A作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承Function.prototype。但是,A调用后返回一个空对象(即 Object实例),所以A.prototype.__proto__指向构造函数(Object)的prototype属性。

//情况三
class A extends null { }  
A.__proto__ === Function.prototype // true 
A.prototype.__proto__ === undefined // true 
//这种情况与第二种情况非常像。A也是一个普通函数,所以直接继承Function.prototype。但是,A调用后返回的对象不继承任何方法,所以它的 __proto__指向Function.prototype,即实质上执行了下面的代码。
class C extends null {   
	constructor() { 
		return Object.create(null); 
	} 
}

20.6 Mixin 多个对象合成一个新的对象,新兑现具有各个成员的接口

function mix(...mixins) {   
	class Mix {} 
	 
	for (let mixin of mixins) {     
		copyProperties(Mix, mixin); // 拷贝实例属性     
		copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性   
	} 
	 
	return Mix; 
} 
 
function copyProperties(target, source) {   
	for (let key of Reflect.ownKeys(source)) {     
		if ( key !== "constructor" && key !== "prototype"&& key !== "name") {       
			let desc = Object.getOwnPropertyDescriptor(source, key);       
			Object.defineProperty(target, key, desc);     
		}   
	} 
} 
//上面代码的mix函数,可以将多个对象合成为一个类。使用的时候,只要继承这个类即可。
class DistributedEdit extends mix(Loggable, Serializable) {   // ... } 

21. Decorator

以 Mixin 为例

//mixins
export function mixins(...list) {   
	return function (target) {     
		Object.assign(target.prototype, ...list);   
	}; 
} 
//可将上述方法作为修饰器,为类加入新方法
import { mixins } from './mixins'; 
 
const Foo = {   
	foo() { 
		console.log('foo') 
	} 
}; 
 
@mixins(Foo) 
class MyClass {} 
 
let obj = new MyClass(); 
obj.foo() // "foo"

22(23). Module

22.1 概述

ES6 实现的模块相当简单,完全可以取代 CommonJs 和 AMD 规范,成为服务器和浏览器通用的模块解决方案
其模块设计思想是尽量静态化,使得编译时就能确定模块的依赖关系及输入输出变量,效率更高
而 CommonJs 和 AMD 只能在运行时确定。例:

// CommonJS模块 
let { stat, exists, readFile } = require('fs'); 
 
// 等同于 
let _fs = require('fs'); 
let stat = _fs.stat; 
let exists = _fs.exists; 
let readfile = _fs.readfile; 

//整体加载 fs 模块(所有方法),生成一个对象(_fs),然后在该对象上读取相应方法,这种加载称为“运行时加载”,因为只有运行时才能得到这个对象

ES6 模块的优势

1. 编译时加载使得能够静态分析,从而进一步拓宽 JS 语法
2. 不再需要UMD模块格式,将来服务器和浏览器都会支持 ES6 模块格式
3. 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者navigator对象的属性
4. 不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供

22.2 严格模式

ES6 默认采用严格模式,不管有没有加 “use strict”
严格模式限制:

  • 变量还必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用 with 语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量 delete prop,会报错,只能删除属性 delete global[prop]
  • eval 不会在它的外层作用域引入变量
  • evalarguments 不能被重新赋值
  • arguments 不会自动反映函数参数的变化
  • 不能使用 arguments.calleearguments.caller
  • 禁止 this 指向全局对象 。ES6 模块之中,顶层的 this 指向 undefined
  • 不能使用 fn.callerfn.arguments 获取函数调用的堆栈
  • 增加了保留字(比如 protectedstaticinterface

22.3 export 命令
export 用于规定模块的对外接口,import 用于引入其他模块
export 两种写法:

export var firstName = 'Michael'; 
export var lastName = 'Jackson'; 
export var year = 1958;

//或
var firstName = 'Michael'; 
var lastName = 'Jackson'; 
var year = 1958;  
export {firstName, lastName, year}; //即使只有一个变量也要用{}包裹,这样才形成一个接口

//默认输出
export default function AA(){}	//default本质上是个变量 后面不能跟变量声明
import aa from 'AA' 	//当导出的是default 时,import中可以自定义名称表示输出的内容
//一个模块只能有一个默认输出,所以import只能对应一个方法,不需要{}

//正常输出
export function AA(){}
import {AA} from 'AA'	//注意大括号

export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值,但 import 里不允许运行时改变引用的对象
CommonJS 模块输出的是值的缓存,不存在动态更新
export 不能放在块级作用于中,会报错,因为处于条件代码块中不能做静态优化,违背了设计初衷

22.4 import 命令
由于import 命令式再编译阶段执行的,所以会变量提升,且不能使用表达式和变量。
其基本语法:

import { lastName as surname } from './profile'; 
// as 用于重命名
// from 指定引用的文件的位置,文件类型可省

import * from './profile'; 
//* :表示整体加载,包含所有输出值,且引入的该对象是静态分析的,不允许运行时改变

//复合使用
export { foo, bar } from 'my_module';
// 等同于 
import { foo, bar } from 'my_module'; 
export { foo, bar };

多次引用同一文件只会执行一次

22.10 import()

  1. 能动态加载文件,可在块级作用域中使用
  2. import() 返回一个 Promise 对象
  3. import() 函数与所加载的模块没有静态连接关系
  4. import() 类似于 Node 的require方法,区别主要是前者是异步加载,后者是同步加载。
//加载多个模块
Promise.all([   
	import('./module1.js'),   
	import('./module2.js'),   
	import('./module3.js'), 
]) 
.then(([module1, module2, module3]) => {    
	··· 
});

23.1 浏览器加载

传统方法:
浏览器按顺序同步加载 JavaScript 脚本
通过添加 defer 、async实现异步加载,避免浏览器堵塞

defer:渲染完后再执行,按写入顺序执行
async:下载完就执行,按下载完成顺序执行,渲染引擎可能会中断渲染

加载规则 :
浏览器加载 ES6 模块,要加入type="module"属性,这时,浏览器知道是ES6 模块,而进行异步加载,且按页面顺序执行,相当于defer

对于外部的模块脚本,有几点需要注意:

  • 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
  • 模块脚本自动采用严格模式,不管有没有声明use strict。
  • 模块之中,可以使用 import 命令加载其他模块(.js 后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用 export 命令输出对外接口。
  • 模块之中,顶层的 this 关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的。
  • 同一个模块如果加载多次,将只执行一次。

23.2. ES6 模块与 CommonJS 模块

主要差异

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
    CommonJS 模块输出的是原始类型的值时会被缓存,输出函数能得到内部变动后的值
    CommonJS 模块加载机制:CommonJS 的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象
    ES6 运行机制:JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行 时,再根据这个只读引用,到被加载的那个模块里面去取值。
    export通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例

  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
    CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

23.3. Node 加载
Node 要求 ES6 模块采用 .mjs 后缀文件名。只要脚本文件里面使用 import 或者 export 命令,那么就必须采用 .mjs 后缀名,require 不能混入使用

ES6 模块加载 CommonJS 模块
CommonJS 模块里的 module.exports 相当于 export default
CommonJS 模块的输出缓存机制,在 ES6 加载方式下依然有效。

//ES6 加载 CommonJS 的不正确的写法 ,readfile方法只有在运行时才获取得到,而import在编译时就执行了
import { readfile } from 'fs';

CommonJS 模块加载 ES6 模块
CommonJS 模块加载 ES6 模块,不能使用require命令,而要使用import()函数。ES6 模块的所有输出接口,会成为输入对象的属性

24 编程风格

24.1 块级作用域

  1. let 取代 var ,两者语义相同,但 let 没有副作用
  2. let 和 const 之间优先选择 const ,在全局环境中,不应该设置变量,只应设置常量
  3. 所有的函数都应该设置为常量

24.2 字符串
静态字符串一律使用单引号或者反引号,不使用双引号
动态字符串使用反引号

24.3 解构赋值

  1. 使用数组成员对变量赋值时,优先使用解构赋值
  2. 函数的参数如果是对象的成员时,优先使用解构赋值
  3. 函数返回多个值,优先使用对象的解构赋值,而不是数组的解构赋值。以便以后添加返回值或更改返回值顺序
// bad 
function processInput(input) {   
	return [left, right, top, bottom]; 
} 
 
// good 
function processInput(input) {   
	return { left, right, top, bottom }; 
} 
 
const { left, right } = processInput(input); 

24.4~9

  1. 单行定义的对象,最后一个成员不以逗号结尾;多行定义的对象,最后一个成员以逗号结尾
  2. 对象尽量静态化,一旦定义就不得随意添加新的属性;非要添加属性要使用 Object.assign 方法
  3. 函数 不要在函数内使用arguments变量,使用rest运算符(…)代替,且可以返回真正的数值
  4. map结构 只有在模拟现实世界的实体对象是才使用 Object ,只需要 key:value 数据结构时使用 Map 结构
  5. 模块 如果只有一个模块就使用 export default ,export default 与普通的 export 不要同时使用;不要在import中使用 “ * ”通配符,因为这样可以确保模块中有 一个默认输出(export default);模块输出一个函数,函数名首字母应该小写,模块输出一个对象,对象名首字母应该大写
// good 
const a = { k1: v1, k2: v2 }; //最后成员无逗号
const b = {   
	k1: v1,   
	k2: v2, //最后成员有逗号
};
//2.
// bad 
const a = {}; 
a.x = 3; 
 
// if reshape unavoidable 
const a = {}; 
Object.assign(a, { x: 3 }); 
 
// good 
const a = { x: null }; 
a.x = 3; 

//5.使用扩展运算符(...)拷贝数组
const itemsCopy = [...items]; 

//3. 函数 
//立即执行函数可以写成箭头函数的形式。
(() => {   
	console.log('Welcome to the Internet.'); 
})();

25. ECMAScript 规格

25.2 相等运算符(==)
算法说明:

1. 如果x不是正常值(比如抛出一个错误),中断执行。 
2. 如果y不是正常值,中断执行。 
3. 如果Type(x)与Type(y)相同,执行严格相等运算x === y。 
4. 如果x是null,y是undefined,返回true。 
5. 如果x是undefined,y是null,返回true。 
6. 如果Type(x)是数值,Type(y)是字符串,返回x == ToNumber(y)的结果。 
7. 如果Type(x)是字符串,Type(y)是数值,返回ToNumber(x) == y的结果。 
8. 如果Type(x)是布尔值,返回ToNumber(x) == y的结果。 
9. 如果Type(y)是布尔值,返回x == ToNumber(y)的结果。 
10. 如果Type(x)是字符串或数值或Symbol值,Type(y)是对象,返回x == ToPrimitive(y)的结果。 
11.  如果Type(x)是对象,Type(y)是字符串或数值或Symbol值,返回ToPrimitive(x) == y的结果。 
12. 返回false。

你可能感兴趣的:(学习手册)