【前端基础知识学习——Javascript&ES6】

前端基础知识学习——Javascript&ES6

数据类型

1 基本数据类型(6种)

1) String
a) 用法:

用于存储和处理文本,使用**’ '" "**。

相同的引号嵌套时,反斜杠转义字符把特殊字符转换为字符串字符,可以使用转义字符转义的特殊字符有:

代码输出
\’单引号
\"双引号
\\反斜杠
\n换行
\r回车
\ttab(制表符)
\b退格符
\f换页符
b) 创建string对象

但它会拖慢执行速度。–不建议使用

c) 属性和方法

JavaScript 在执行方法和属性时可以把原始值当作对象

属性:length 属性返回字符串的长度

				var txt = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        var sln = txt.length;
        console.log(sln, 'sln'); // 26

方法:

(1) indexOf() \ lastIndexOf() \ search()

// indexOf() 方法返回字符串中指定文本首次出现的索引(位置)
        var str = "The full name of China is the People's Republic of China.";
        var pos = str.indexOf("China");
        console.log(pos, 'pos'); // 17
// lastIndexOf() 方法向后进行检索(从尾到头)

如果未找到文本, indexOf()lastIndexOf() 均返回 -1

// search() 方法搜索特定值的字符串,并返回匹配的位置
				var pos = str.search("China");
				console.log(pos, 'pos'); // 17

indexOf()search()区别:

  • search() 方法无法设置第二个开始位置参数。
  • indexOf() 方法无法设置更强大的搜索值(正则表达式)。

(2) 提取部分字符串的三种的方法:

  • slice(start, end)

    // slice() 提取字符串的某个部分并在新字符串中返回被提取的部分
    // 两个参数:起始索引(开始位置),终止索引(结束位置)
            var str = "Apple, Banana, Mango";
            var res = str.slice(7,13);
            console.log(res); // Banana
            var res1 = str.slice(-13,-7); // 参数可以为负数
            console.log(res1); // Banana
            var res2 = str.slice(7); //省略第二个参数,将裁剪字符串的剩余部分
            console.log(res2); // Banana, Mango
    
  • substring(start, end)

    substring() 类似于 slice(),但 substring() 无法接受负的索引。

  • substr(start, length)

substr() 类似于 slice(),但第二个参数规定被提取部分的长度且不能为负。

(3) 替换字符串内容

// replace() 方法用另一个值替换在字符串中指定的值,只替换首个匹配
        str = "Please visit Microsoft and Microsoft!";
        var n = str.replace("Microsoft", "W3School");
        console.log(n); // Please visit W3School and Microsoft!
// 对大小写敏感,使用正则表达式 /i解决。
        var n1 = str.replace("MICROSOFT", "W3School");
        console.log(n1); // Please visit Microsoft and Microsoft!
        var n2 = str.replace(/MICROSOFT/i, "W3School");
        console.log(n2); // Please visit W3School and Microsoft!
// 如需替换所有匹配,请使用正则表达式的 g 标志(用于全局搜索):
        var n3 = str.replace(/Microsoft/g, "W3School");
        console.log(n3); // Please visit W3School and W3School!

(4) 转换为大写和小写

// toUpperCase() 把字符串转换为大写
        var text1 = "Hello World!";       // 字符串
        var text2 = text1.toUpperCase();  // text2 是被转换为大写的 text1
        console.log(text2); // HELLO WORLD!
// toLowerCase() 把字符串转换为小写

(5) concat() 方法

// concat() 连接两个或多个字符串:
        var text1 = "Hello";
        var text2 = "World";
        text3 = text1.concat(" ",text2);
        console.log(text3); // Hello World

concat() 方法可用于代替加运算符。注意:所有字符串方法都会返回新字符串。它们不会修改原始字符串。

(6) 提取字符串字符

  • charAt(position)

    // charAt() 方法返回字符串中指定下标(位置)的字符串:
            var str = "HELLO WORLD";
            console.log(str.charAt(0)); // 返回 H
    
  • charCodeAt(position)

    charCodeAt() 方法返回字符串中指定索引的字符 unicode 编码

(7) 把字符串转换为数组

// split() 将字符串转换为数组
        var txt = "a,b,c,d,e";   // 字符串
        console.log(txt.split(",")); // ['a', 'b', 'c', 'd', 'e']
// 如果省略分隔符,被返回的数组将包含 index [0] 中的整个字符串
				txt.split(); //['a,b,c,d,e']
// 如果分隔符是 "",被返回的数组将是间隔单个字符的数组
				txt.split(""); //['a', ',', 'b', ',', 'c', ',', 'd', ',', 'e']
2) Number
// 极大或极小的数字可以通过科学(指数)计数法  ”e+数字“代表”0“的个数
        var y = 123e5;
        console.log(y, 'y'); // 12300000
        var z = 123e-5;
        console.log(z, 'z'); // 0.00123

number方法:

方法描述
parseFloat()解析一个字符串,并返回一个浮点数。
parseInt()解析一个字符串,并返回一个整数。
3) Boolean

值为false的情况:

  • 0
  • -0
  • null
  • false
  • -NaN
  • undefined
  • 空字符串「""

否则为true其值不是undefinednull的任何对象「所有的对象类型」(包括其值为false的布尔对象)在传递给条件语句时都将计算为true。

const f1 = new Boolean(false); if(f1){console.log('f11111');} // f11111
4) Null
5) Undefined
1.定义:

undefined原理上可以说是没有找到,null原理上意思为空对象。

2、null和undefined的区别

a) 数据类型不同

console.log(typeof null) // object
console.log(typeof undefined) // undefined

b) null和undefined 两者相等,但是当两者做全等比较时,两者又不等。(因为它们的数据类型不一样)

console.log(null == undefined) // true
console.log(null === undefined) // false

c)转化成数字的值不同

console.log(Number(null)) // 0
console.log(Number(undefined)) // NaN
console.log(22+null) // 22
console.log(22+undefined) // NaN

d) null代表"空",代表空指针;undefined是定义了没有赋值

var a;
console.log(a); // undefined

var b=null;
console.log(b) // null
3.运用场景
undefined

a) 变量被声明但没有赋值时,就等于undefined

b) 调用函数时,应该提供的参数没有提供,该参数等于undefined

	function f(a,b) {
          console.log(a,b)
      }
      f("你好");   //输出结果为:你好 undefined

c) 对象没有赋值的属性,该属性的值为undefined

	var obj = {
          name:"lihua",
          age:"18"
      }
      console.log(obj.sex)  //输出结果为: undefined

d) 函数没有返回值时,默认返回undefined

  	function  add(a,b) {
         var c = a+b;
         //  没有返回时为undefined 
         //return c;
     }
     console.log(add(1,2));  //输出结果为:undefined
null

a) 作为函数的参数,表示该函数的参数不是对象(不想传递参数)

     function  add() {
         console.log(111)
     }
     add(null);

b) 对象原型链的终点

c) 定义的变量准备在将来用于保存对象,此时可以将该变量初始化为null

var a = null;
console.log(a);//null
6) Symbol

2 引用数据(5种)

1) Object
// 对象:键值对的形式存在(name : value),属性逗号分割
        var person = {id:111, name:"xxx", age:18};
// 两种寻址形式:
        id = person.id;
        name = person["name"]
        console.log(id, name, "id name");
2) Array

创建的两种方式:

	// 方法一:
        var car1 = new Array();
        car1[0] = "aaa";
        car1[1] = "bbb";
        car1[2] = "ccc";
        console.log(car1, 'car1');
  // 方法二:
        var car2 = new Array("aa", "bb", "cc");
        console.log(car2, 'car2');
3) Function
4) RegExp
a) 语法格式
// 写法一
var patt=new RegExp(pattern,modifiers);
// 写法二
var patt=/pattern/modifiers;
pattern(模式) 描述了表达式的模式
modifiers(修饰符) 用于指定全局匹配、区分大小写的匹配和多行匹配
b) 修饰符
修饰符描述
i执行对大小写不敏感的匹配。
g执行全局匹配(查找所有匹配而非在找到第一个匹配后停止)。
m执行多行匹配。
c) 模式
方括号
表达式描述
[abc]查找方括号之间的任何字符。
[0-9]查找任何从 0 至 9 的数字。
(x|y)查找任何以 | 分隔的选项。
元字符
元字符描述
\d查找数字。
\s查找空白字符。
\b匹配单词边界。
\uxxxx查找以十六进制数 xxxx 规定的 Unicode 字符。
量词
量词描述
n+匹配任何包含至少一个 n 的字符串。
n*匹配任何包含零个或多个 n 的字符串。
n?匹配任何包含零个或一个 n 的字符串。
				reg = /r{3}/; // rrr                     {n} 出现n次
        reg = /(re){3}/; //rerere                
        reg = /re{2,4}g/; // reeg reeeg reeeeg   {m,n} 出现m次到n次 a{2,4}
        reg = /re{2,}g/; //reeeeeg               {n,} 出现n次以上 a{2,}
        reg = /re+g/; // reg reeeg               + 至少出现一次
        reg = /re*g/; //rg reg reeeg             * 出现零次或则零次以上 >=0
        reg = /re?g/; //rg reg                   ? 出现一次或则零次
d) 支持正则表达式的String对象的方法

search() - 检索与正则表达式相匹配的值.

match() - 找到一个或多个正则表达式的匹配.

replace() - 替换与正则表达式匹配的子串.

split() - 把字符串分割为字符串数组.

e) 常用正则(参考):
		验证Email地址:^\w+[-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$
        验证身份证号(15位或18位数字):^\d{15}|\d{}18$
        中国大陆手机号码:1\d{10}
        中国大陆固定电话号码:(\d{4}-|\d{3}-)?(\d{8}|\d{7})
        中国大陆邮政编码:[1-9]\d{5}
        IP地址:((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)
        日期(年-月-日):(\d{4}|\d{2})-((1[0-2])|(0?[1-9]))-(([12][0-9])|(3[01])|(0?[1-9]))
        日期(月/日/年):((1[0-2])|(0?[1-9]))/(([12][0-9])|(3[01])|(0?[1-9]))/(\d{4}|\d{2})
        验证数字:^[0-9]*$
        验证n位的数字:^\d{n}$
        验证至少n位数字:^\d{n,}$
        验证m-n位的数字:^\d{m,n}$
        验证零和非零开头的数字:^(0|[1-9][0-9]*)$
        验证有1-3位小数的正实数:^[0-9]+(.[0-9]{1,3})?$
        验证非零的正整数:^\+?[1-9][0-9]*$
        验证非零的负整数:^\-[1-9][0-9]*$
        验证非负整数(正整数 + 0) ^\d+$
        验证非正整数(负整数 + 0) ^((-\d+)|(0+))$
        验证长度为3的字符:^.{3}$
        验证由26个英文字母组成的字符串:^[A-Za-z]+$
        验证由26个大写英文字母组成的字符串:^[A-Z]+$
        验证由26个小写英文字母组成的字符串:^[a-z]+$
        验证由数字和26个英文字母组成的字符串:^[A-Za-z0-9]+$

img

5) Date

创建 Date 对象: new Date()

Date方法:

方法描述
getDate()从 Date 对象返回一个月中的某一天 (1 ~ 31)。
getDay()从 Date 对象返回一周中的某一天 (0 ~ 6)。
getFullYear()从 Date 对象以四位数字返回年份。
getHours()返回 Date 对象的小时 (0 ~ 23)。
getMilliseconds()返回 Date 对象的毫秒(0 ~ 999)。
getMinutes()返回 Date 对象的分钟 (0 ~ 59)。
getMonth()从 Date 对象返回月份 (0 ~ 11)。
getSeconds()返回 Date 对象的秒数 (0 ~ 59)。
getTime()返回 1970 年 1 月 1 日至今的毫秒数。

3 类型转换

1)使用 typeof 操作符来查看 JavaScript 变量的数据类型
				typeof NaN                    // NaN 的数据类型是 number
        typeof [1,2,3,4]              // 数组(Array)的数据类型是 object
        typeof new Date()             // 日期(Date)的数据类型为 object
        typeof myCar                  // 未定义变量的数据类型为 undefined
        typeof null                   // null 的数据类型是 object
2)constructor 属性

Array和Date返回都是object,不能直接用typeof进行判断,考虑使用constructor属性。

constructor 属性返回所有 JavaScript 变量的构造函数。

				[1,2,3,4].constructor              // 返回函数 Array()   { [native code] }
				{name:'John', age:34}.constructor  // 返回函数 Object()  { [native code] }
				new Date().constructor             // 返回函数 Date()    { [native code] }
				function () {}.constructor         // 返回函数 Function(){ [native code] }
3)类型转换
a) Number() 转换为数字

字符串:

				Number("3.14")    // 返回 3.14
				Number("")        // 返回 0
				Number("99 88")   // 返回 NaN

布尔值:

				Number(false)     // 返回 0
				Number(true)      // 返回 1

日期:

        d = new Date();
        console.log(Number(d));   // 1656318884636
        console.log(d.getTime()); // 1656318884636
        // 日期转数字时,Number()和日期方法 getTime()有相同的效果 
b) String() 转换为字符串

数字、布尔值、日期可以方法toString()转换字符串。

c) Boolean() 转换为布尔值
d) 自动类型转换
字符串 +是拼接  -是运算 
"5" + null  // 返回"5null"   null 转换为 "null"
"5" + 1     // 返回 "51"      1 转换为 "1" 
"5" - 1     // 返回 4         "5" 转换为 5

不同的数值转换为数字(Number), 字符串(String), 布尔值(Boolean)注意情况

原始值转换为数字转换为字符串转换为布尔值
“0”0“0”true
“000”0“000”true
“”0“”false
[ ]0“”true
[20]20“20”true
null0“null”false

运算符

1 算术运算符

+加 -减 *乘 /除 %取余 ++自增 --自减

y=5 加在前先自增再赋值
x=++y; // x=6,y=6
x=y++; // x=5,y=6
// + 运算符 用于字符串拼接/加法运算
// 数字相加,返回数字相加的和,如果数字与字符串相加,返回字符串
        x=5+5;
        y="5"+5;
        z="Hello"+5;
        console.log(x, y, z); // 10 '55' 'Hello5'

2 赋值运算符

= += -= *= /= %=

3 比较运算符

== === != !== > < >= <= 一般和条件语句结合使用。

4 逻辑运算符

&&(and) ||(or) !(not)

5 三元运算符

条件表达式? 表达式1(true) : 表达式2(false);

条件语句

if 语句 - 只有当指定条件为 true 时,使用该语句来执行代码

if (true){执行}

if…else 语句 - 当条件为 true 时执行代码,当条件为 false 时执行其他代码

if…else if…else 语句- 使用该语句来选择多个代码块之一来执行

switch 语句 - 使用该语句来选择多个代码块之一来执行

// 使用 break 来阻止代码自动地向下一个 case 运行
// 使用 default 关键词来规定匹配不存在时做的事情

错误

try 语句测试代码块的错误。

catch 语句处理错误。

throw 语句创建自定义错误:创建或抛出异常(exception)。

finally 语句在 try 和 catch 语句之后,无论是否有触发异常,该语句都会执行。

try {
    ...  throw...   //异常的抛出
} catch(e) {
    ...    //异常的捕获与处理
} finally {
    ...    //结束处理
}

保留关键字

注意:一些标识符是保留关键字,不能用作变量名或函数名。

ES5中新增的保留关键字
classenumexportextendsimportsuper
JavaScript 对象、属性和方法
ArrayDateevalfunctionhasOwnProperty
InfinityisFiniteisNaNisPrototypeOflength
MathNaNnameNumberObject
prototypeStringtoStringundefinedvalueOf
Java 保留关键字\Windows 保留关键字\HTML 事件句柄\非标准 JavaScript:const

this关键字

JavaScript 中 this 会随着执行环境的改变而改变。

  • 在方法中,this 表示该方法所属的对象。

    				var person = {
                firstName: "John",
                lastName : "Doe",
                id       : 5566,
                fullName : function() { // 创建了fullName方法
                  return this.firstName + " " + this.lastName; // this指向方法所属对象person
                }
            };
    
  • 如果单独使用,this 表示全局对象。

            var x = this;
            console.log(x, '单独使用的this'); // window
    
            "use strict";
            var y = this;
            console.log(y, '严格模式下 单独使用的this'); // window
    
  • 在函数中,this 表示全局对象。

    				function myFunction1() {
              return this; // [object Window]
            }
    
  • 在函数中,在严格模式下,this 是未定义的(undefined)。

            "use strict";
            document.getElementById("demo1").innerHTML = myFunction1();
            function myFunction1() {
              return this; // undefined  
            }
    
  • 在事件中,this 表示接收事件的元素。

    				<button onclick="this.style.display='none'"> // this指向button元素
    				点我后我就消失了
    				</button>
    
  • 类似 call() 和 apply() 方法可以将 this 引用到任何对象。

    				var person1 = {
              fullName: function() {
                return this.firstName + " " + this.lastName;
              }
            }
            var person2 = {
              firstName:"John",
              lastName: "Doe",
            }
            person1.fullName.call(person2); // 返回 "John Doe"
    // 实例中this指向了person2,即使它是person1的方法。
    

    区别:接收参数的方式不同

        function add(c,d){
            return this.a + this.b + c + d;
        }
    
        var s = {a:1, b:2};
        console.log(add.call(s,3,4)); // 1+2+3+4 = 10   
        console.log(add.apply(s,[5,6])); // 1+2+5+6 = 14 
    
    

    apply可以将一个数组默认的转换为一个参数列表([param1,param2,param3] 转换为 (param1,param2,param3),产生了一些妙用:

    a) Math.max 可以实现得到数组中最大的一项

    		var array = [1,2,3,4,5];
        var max=Math.max.apply(null,array); // 5
    

    b) Math.min 可以实现得到数组中最小的一项

    		var min=Math.min.apply(null,array); // 1
    

    c) Array.prototype.push 实现两个数组合并

JSON

JavaScript Object Notation是一种轻量级的数据交换格式,采用完全独立于语言的文本格式,用于存储和传输数据。

  • JSON 语法规则:
	数据为 键/值 对。 // "name":"Runoob"
	数据由逗号分隔。 // {"name":"Runoob", "url":"www.runoob.com"}
	大括号保存对象。
	方括号保存数组
函数描述
JSON.parse()用于将一个 JSON 字符串转换为 JavaScript 对象。
JSON.stringify()用于将 JavaScript 值转换为 JSON 字符串。

函数定义

  • 函数声明语法,使用关键字 function 定义函数。

    	function functionName(parameters) {
      		执行的代码
    	}
    
  • JavaScript 函数可以通过一个表达式定义,即函数表达式可以存储在变量中,变量可作为函数使用。

    	var x = function (a, b) {return a * b};
    	document.getElementById("demo").innerHTML = x(4, 3); // 12
    // 以上匿名函数,不需要函数名称,通常通过变量名来调用。
    
  • Function构造函数

    	var myFunction = new Function("a", "b", "return a * b");
      var x = myFunction(4, 3); //12
    // 下面的功能等同于上面的,尽量用下面这种方式
      var myFunction1 = function (a, b) {return a * b};
      var y = myFunction1(4, 3);
    
  • 函数提升(Hoisting)

    函数可以在声明之前调用

    	myFunction(5);
    	function myFunction(y) {
        	return y * y;
    	}
    

    使用表达式定义函数时无法提升

    		x3(5); // x3 is not a function
        var x3 = function myFunction3(y) {
            return y * y;
        }
    
  • 自调用函数

    不能自调用声明的函数,是一个匿名函数的自我调用。

    	(function () {
        	var x = "Hello!!";      // 我将调用自己
    	})(); 
    
  • 函数是对象

    typeof 函数类型 ——“function”. 准确描述为对象,有属性和方法

    arguments.length 属性返回函数调用过程接收到的参数个数

    function myFunction(a, b) {
        return arguments.length; // 2 两个参数a和b
    }
    

    toString() 方法将函数作为一个字符串返回

    function myFunction(a, b) {
        return a * b;
    }
    var txt = myFunction.toString(); //以字符串function myFunction(a, b) { return a * b; }返回
    
  • 箭头函数 — ES6新增

    // 原始版(未用箭头函数)
    hello = function() {
      return "Hello World!";
    }
    
    // 采用箭头函数
    hello1 = () => {
    	return "Hello World!1";
    }
    
    // 函数只有一个语句,并且该语句返回一个值,则可以去掉括号和 return 关键字
    // 箭头函数默认返回值
    hello = () => "Hello World!";
    
    // 有参数,则将它们传递到括号内,带参数的箭头函数
    hello = (val) => "Hello " + val;
    
    // 只有一个参数,可以略过括号,不带括号的箭头函数
    hello = val => "Hello " + val;
    

    箭头函数的this指向问题—对于箭头函数,this 关键字始终表示定义箭头函数的对象

    const hello = () => {
    	document.getElementById("demo5").innerHTML += this;
    }
    //window 对象调用函数:
    window.addEventListener("load", hello); // [object Window]
    //button 对象调用函数:
    document.getElementById("btn").addEventListener("click", hello); // [object Window]
    

    声明时,使用 const 比使用 var 更安全。

函数参数

  • 分类

    显式参数(Parameters)

    functionName(parameter1, parameter2, parameter3) {
        // 要执行的代码……
    }
    

    隐式参数(Arguments)

    function findMax() {
                var x, max = arguments[0];
                if (arguments.length < 2) return max;
                for(x = 0; x < arguments.length; x++) {
                    if(arguments[x] > max){
                        max = arguments[x];
                    }
                }
                return max;
    }
    x = findMax(1,2,3,4,500,6); // 输出为:500
    
  • 默认参数

    ES5 中如果函数在调用时未提供隐式参数,参数会默认设置为: undefined

    function myFunction(x, y) {
        y = y || 0;
    }
    

    如果 y 已经定义,y || 0 返回 y,因为 y 是 true,否则返回 0,因为 undefined 为 false。

函数调用(4种方式)

  • 作为一个函数调用

    function myFunction(a, b) { // 这个函数不属于任何对象,但是默认的全局对象
        return a * b;
    }
    myFunction(10, 2); // 等同于window.myFunction(10,2);
    
  • 函数作为方法调用

    var myObject = { // 创建myObject对象
        firstName:"John", // firstName属性
        lastName: "Doe", // lastName属性
        fullName: function () {   
            return this.firstName + " " + this.lastName;
        }
    }
    myObject.fullName();         // 返回 "John Doe"
    // fullName方法是一个函数,属于myObject对象,是this的指向。
    

    注:函数作为对象方法调用,会使 this 的值成为对象本身

    var myObject = {
        firstName:"John",
        lastName: "Doe",
        fullName: function () {
            return this;
        }
    }
    myObject.fullName();          // 返回 [object Object] (所有者对象)
    
  • 使用构造函数调用函数

    构造函数的调用会创建一个新的对象。新对象会继承构造函数的属性和方法

  • 作为函数方法调用函数

    call()apply() 用于调用函数时,其第一个参数必须是对象本身。

​ 两者的区别在于第二个参数:

​ a) apply传入的是一个参数数组,也就是将多个参数组合成为一个数组传入

​ b) call是作为call的参数传入(从第二个参数开始)

Number(数字)

  • 八进制和十六进制

    如果前缀为 0,则 JavaScript 会把数值常量解释为八进制数,如果前缀为 0 和 “x”,则解释为十六进制数

    var y = 0377; // 255
    var z = 0xFF; // 255
    
  • 无穷大 Infinity(+/-)

  • 非数字值 NaN

    isNaN(1); // 是数字 false
    isNaN("apple"); // 不是数字 true
    
    • 0 / 0 得 NaN 。上面也有一些得 NaN 的情况。
    • NaN 加、减、乘或除以任何数(包括 Infinity 、 -Infinity 和 NaN 本身)得 NaN 。
    • 无论变量 x 取何值(包括 undefined 、 null 、 true 、 false 、 Infinity 、 -Infinity 和 NaN 本身),NaN == x 恒为假。NaN.isNaN() 为真。 NaN.isFinite() 为假。 Boolean(NaN) 为假。
  • 数学方法

    方法描述
    Number.parseFloat()将字符串转换成浮点数,和全局方法 parseFloat() 作用一致。
    Number.parseInt()将字符串转换成整型数字,和全局方法 parseInt() 作用一致。
    Number.isFinite()判断传递的参数是否为有限数字。
    Number.isInteger()判断传递的参数是否为整数。
    Number.isNaN()判断传递的参数是否为 isNaN()。
  • 数字类型原型上的一些方法

    方法描述
    toExponential()返回一个数字的指数形式的字符串,如:1.23e+2
    toFixed()返回指定小数位数的表示形式。var a=123; b=a.toFixed(2); // b="123.00"
    toPrecision()返回一个指定精度的数字。如下例子中,a=123 中,3会由于精度限制消失:var a=123; b=a.toPrecision(2); // b="1.2e+2"

String(字符串)

  • 使用位置(索引)可以访问字符串中任何的字符

    var carname="Volvo XC60";
    var character=carname[7]; // C
    
  • 内容匹配 str.match(目标内容) // 找到的话返回目标内容

  • 替换内容 str.replace(目标内容,替换内容)

  • 字符串转为数组 str.split(“分隔符号”)

Array(数组)

  • 创建数组的三种方式

    • 常规方式

      var myCars=new Array();
      myCars[0]="a";      
      myCars[1]="b";
      myCars[2]="c";
      
    • 简洁方式

      var myCars=new Array("a","b","c"); // 不常用
      
    • 字面

      var myCars=["a","b","c"]
      
  • Array对象方法

    方法描述
    concat()连接两个或更多的数组,并返回结果。不会改变原始数组
    arr1.concat(arr2,arr3,…)
    copyWithin()从数组的指定位置拷贝元素到数组的另一个指定位置中。
    array.copyWithin(target, start, end)
    entries()返回数组的可迭代对象。
    every()检测数值元素的每个元素是否都符合条件。不会改变原始数组
    array.every(function(currentValue,index,arr), thisValue)
    fill()使用一个固定值来填充数组。
    array.fill(value, start, end)
    filter()检测数值元素,并返回符合条件所有元素的数组。不会改变原始数组
    array.filter(function(currentValue,index,arr), thisValue)
    find()返回符合传入测试(函数)条件的数组元素。返回第一个满足条件的,之后不再执行,没有满足项返回undefined
    findIndex()返回符合传入测试(函数)条件的数组元素索引。返回第一个满足条件的,之后不再执行,没有满足项返回-1
    forEach()数组每个元素都执行一次回调函数。
    from()通过给定的对象中创建一个数组。
    includes()判断一个数组是否包含一个指定的值。
    indexOf()搜索数组中的元素,并返回它所在的位置。
    array.indexOf(item,start)
    isArray()判断对象是否为数组。
    Array.isArray(obj)
    join()把数组的所有元素放入一个字符串。
    keys()返回数组的可迭代对象,包含原始数组的键(key)。
    lastIndexOf()搜索数组中的元素,并返回它最后出现的位置。
    map()通过指定函数处理数组的每个元素,并返回处理后的数组。不会改变原始数组
    pop()删除数组的最后一个元素并返回删除的元素
    push()向数组的末尾添加一个或更多元素,并返回新的长度
    reduce()将数组元素计算为一个值(从左到右)。
    reduceRight()将数组元素计算为一个值(从右到左)。
    reverse()反转数组的元素顺序。
    shift()删除并返回数组的第一个元素
    slice()选取数组的一部分,并返回一个新数组。不会改变原始数组
    array.slice(start, end)截取第start个(包含)到第end个(不包含)中的元素
    some()检测数组元素中是否有元素符合指定条件。不会改变原始数组
    执行时:
    如果有一个元素满足条件,则表达式返回true , 剩余的元素不会再执行检测。
    如果没有满足条件的元素,则返回false。
    sort()对数组的元素进行排序。
    splice()从数组中添加或删除元素。
    array.splice(index,howmany,item1,…,itemX)
    返回值:如果从 arrayObject 中删除了元素,则返回的是含有被删除的元素的数组
    toString()把数组转换为字符串,并返回结果。
    unshift()向数组的开头添加一个或更多元素,并返回新的长度
    valueOf()返回数组对象的原始值。

Math(算数)

  • Math对象属性

    属性描述
    E返回算术常量 e,即自然对数的底数(约等于2.718)。
    LN2返回 2 的自然对数(约等于0.693)。
    LN10返回 10 的自然对数(约等于2.302)。
    LOG2E返回以 2 为底的 e 的对数(约等于 1.4426950408889634)。
    LOG10E返回以 10 为底的 e 的对数(约等于0.434)。
    PI返回圆周率(约等于3.14159)。
    SQRT1_2返回 2 的平方根的倒数(约等于 0.707)。
    SQRT2返回 2 的平方根(约等于 1.414)。
  • Math对象方法

    方法描述
    round(x)四舍五入。
    random(x)返回 0 ~ 1 之间的随机数。
    max(x)返回最大值
    min(x)返回最小值

ES6相关内容

let和const命令

1.let命令
  • 基本使用

    {
          let a = 10;
          var b = 1;
    }       
    console.log(a); // ReferenceError: a is not defined.
    console.log(b); // 1
    

    let只在代码块内有效,适合场景:for循环计数器

    for (let i = 0; i < 10; i++) {
      // ...
    }
    console.log(i);// ReferenceError: i is not defined
    外面打印会报错
    

    for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。

    for (let i = 0; i < 3; i++) {
      let i = 'abc';
      console.log(i);
    }
    // abc
    // abc
    // abc
    

    说明函数内部的变量i和循环变量i不在同一个作用域,有各自的作用域。(同一作用域不可以使用let重复声明同一个变量

  • 不存在变量提升

    // var 的情况
    console.log(foo); // 输出undefined
    var foo = 2;
    
    // let 的情况
    console.log(bar); // 报错ReferenceError
    let bar = 2;
    

    和var可以“变量提升不同”,let必须先声明后使用

  • 暂时性死区(temporal dead zone,简称 TDZ)

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

    if (true) {
      // TDZ开始
      tmp = 'abc'; // ReferenceError
      console.log(tmp); // ReferenceError
    
      let tmp; // TDZ结束
      console.log(tmp); // undefined
    
      tmp = 123;
      console.log(tmp); // 123
    }
    

    影响:typeof不再是一个百分之百安全的操作。

    typeof x; // ReferenceError
    let x;
    // 变量x使用let命令声明,所以在声明之前,都属于x的“死区”,只要用到该变量就会报错。
    
    typeof undeclared_variable // "undefined"
    // undeclared_variable是一个不存在的变量名,结果返回“undefined”,没有let不报错。
    
2.块级作用域
  • 产生原因(不合理场景)

    • 内层变量覆盖外层变量

      var tmp = new Date();
      
      function f() {
        console.log(tmp);
        if (false) {
          var tmp = 'hello world';
        }
      }
      
      f(); // undefined
      
    • 用来计数的循环变量泄露为全局变量

      var s = 'hello';
      
      for (var i = 0; i < s.length; i++) {
        console.log(s[i]);
      }
      
      console.log(i); // 5
      // 变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。
      
  • ES6块级作用域

    function f1() {
      let n = 5;
      if (true) {
        let n = 10;
      }
      console.log(n); // 5 打印的外层作用域,不受内层影响
    }
    
  • 块级作用域与函数声明

    ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。

    在ES6浏览器的实现行为方式:

    1.允许在块级作用域内声明函数。
    2.函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
    3.函数声明会提升到所在的块级作用域的头部。
    

    注意:避免在块级作用域内声明函数。如果确实需要,应写成函数表达式,而不是函数声明语句。

    // 块级作用域内部的函数声明语句,建议不要使用
    {
      let a = 'secret';
      function f() {
        return a;
      }
    }
    
    // 块级作用域内部,优先使用函数表达式
    {
      let a = 'secret';
      let f = function () {
        return a;
      };
    }
    

    ES6 的块级作用域必须有大括号

    // 第一种写法,报错  原因:不存在块级作用域,let只能出现在当前作用域的顶层
    if (true) let x = 1;
    
    // 第二种写法,不报错
    if (true) {
      let x = 1;
    }
    
3.const命令
  • 基本用法

    • 声明一个只读的常量。一旦声明,常量的值就不能改变

      const PI = 3.1415;
      PI // 3.1415
      
      PI = 3; 
      // TypeError: Assignment to constant variable.  改变常量值会报错
      
    • 变量声明后必须立即初始化

      const foo;
      // SyntaxError: Missing initializer in const declaration   只声明不赋值会报错
      
    • const的作用域与let命令相同:只在声明所在的块级作用域内有效

    • const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。

    • const声明的常量,也与let一样不可重复声明

  • 本质

    const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。

    • 对简单数据类型:值就保存在变量指向的那个内存地址,因此等同于常量。

    • 对于复合类型的数据(主要是对象和数组):变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的

      const foo = {};
      
      // 为 foo 添加一个属性,可以成功
      foo.prop = 123;
      foo.prop // 123
      
      // 将 foo 指向另一个对象,就会报错
      foo = {}; // TypeError: "foo" is read-only
      

变量的解构赋值

1.数组的解构赋值
  • 基本用法

    语法格式(“模式匹配”):按次序排列的,变量的取值由它的位置决定

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

    解构不成功,变量的值就等于undefined

  • 默认值

    ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined,默认值才会生效。

    let [x = 1] = [undefined];    // let [x = 1] = [undefined];
    x // 1   生效
    
    let [x = 1] = [null];
    x // null    不生效
    

    如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。

    function f() {
      console.log('aaa');
    }
    let [x = f()] = [1]; // x能取到值,函数f不会执行
    
2.对象的解构赋值
  • 基本用法

    对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

    let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
    foo // "aaa"
    bar // "bbb"
    
    let { baz } = { foo: 'aaa', bar: 'bbb' };
    baz // undefined  解构失败变量的值为undefined
    

    对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。

    let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
    baz // "aaa"   
    foo // error: foo is not defined
    // foo 是匹配模式,baz才是真正被赋值的变量
    
  • 默认值

    默认值生效的条件是,对象的属性值严格等于undefined

    var {x = 3} = {x: undefined};
    x // 3
    
    var {x = 3} = {x: null};
    x // null
    

    注意:

    • 已声明的变量用于解构赋值

      // 错误的写法
      let x;
      {x} = {x: 1};
      // SyntaxError: syntax error 只有不将大括号写在首行才能避免javascript将其理解为代码块
      
      // 正确的写法
      let x;
      ({x} = {x: 1});
      
    • 解构赋值允许等号左边的模式之中,不放置任何变量名

      ({} = [true, false]);
      ({} = 'abc');
      ({} = []); // 都是合法语法,可以执行
      
    • 可以对数组进行对象属性的解构

      let arr = [1, 2, 3];
      let {0 : first, [arr.length - 1] : last} = arr;
      first // 1
      last // 3
      
3.字符串的解构赋值

字符串解构赋值时,字符串被转换成了一个类似数组的对象

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

let {length : len} = 'hello'; // 字符串也有length属性,可以对其进行解构赋值
len // 5
4.数值和布尔值的解构赋值

解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。

let {toString: s} = 123;
s === Number.prototype.toString // true

let {toString: s} = true;
s === Boolean.prototype.toString // true

由于undefinednull无法转为对象,所以对它们进行解构赋值,都会报错。

let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
5.函数参数的解构赋值
function add([x, y]){ // 传参时,数组参数被解构成变量x和y。
  return x + y;
}

add([1, 2]); // 3

函数参数的解构也可以使用默认值。

[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]  undefined会触发函数参数的默认值。
6.圆括号问题
  • 不能使用的三种情况

    • 变量声明语句

      let [(a)] = [1]; //报错
      
    • 函数参数

      function f([(z)]) { return z; } // 报错
      
    • 赋值语句的模式

      ([a]) = [5]; // 整个模式在括号中,报错
      [({ p: a }), { x: c }] = [{}, {}]; // 部分在括号中,报错
      
  • 可以使用圆括号的情况

    只有一种:赋值语句的非模式部分,可以使用圆括号

    // 下面三种都可以正确执行
    [(b)] = [3]; // 模式是取数组的第一个成员,与圆括号无关
    ({ p: (d) } = {}); //模式是p,不是d
    [(parseInt.prop)] = [3]; // 模式是取数组的第一个成员,与圆括号无关
    
7.用途
  • 交换变量的值

    let x = 1;
    let y = 2;
    [x, y] = [y, x]
    
  • 从函数返回多个值

    函数只能返回一个值,返回多个值的时候把它们放在数组或对象里返回,可以通过解构赋值方便地取用

    // 返回一个数组
    function example() {
    		return [1, 2, 3];
    }
    let [a, b, c] = example();
    
    // 返回一个对象
    function example() {
      return {
        foo: 1,
        bar: 2
      };
    }
    let { foo, bar } = example();
    
  • 函数参数的定义

  • 提取JSON数据

    let jsonData = {
      id: 42,
      status: "OK",
      data: [867, 5309]
    };
    
    let { id, status, data: number } = jsonData;
    
    console.log(id, status, number);
    // 42, "OK", [867, 5309]
    
  • 函数参数的默认值

  • 遍历Map解构

    // 获取键名
    for (let [key] of map) {
      // ...
    }
    
    // 获取键值
    for (let [,value] of map) {
      // ...
    }
    
  • 输入模块的指定方法

    const { SourceMapConsumer, SourceNode } = require("source-map");
    

字符串的扩展

1.字符的 Unicode 表示法

ES6允许采用\uxxxx形式表示一个字符,其中xxxx表示字符的 Unicode 码点

只要将码点放入大括号内就可以解决 只限于码点在\u0000~\uFFFF之间的字符 的问题。

有了这种表示法之后,JavaScript 共有 6 种方法可以表示一个字符

'\z' === 'z'  // true
'\172' === 'z' // true
'\x7A' === 'z' // true
'\u007A' === 'z' // true
'\u{7A}' === 'z' // true
2.字符串的遍历器接口

ES6 为字符串添加了遍历器(Iterator)接口,使得字符串可以被for...of循环遍历

优点:可以识别大于0xFFFF的码点

let text = String.fromCodePoint(0x20BB7);

for (let i = 0; i < text.length; i++) {
  console.log(text[i]);
}
// " "
// " "

for (let i of text) {
  console.log(i);
}
// "𠮷"

上面代码中,字符串text只有一个字符,但是for循环会认为它包含两个字符(都不可打印),而for...of循环会正确识别出这一个字符

3.模板字符串

模板字符串(template string)是增强版的字符串,用反引号(``)标识。

它可以当作普通字符串使用

`In JavaScript '\n' is a line-feed.`

也可以用来定义多行字符串

`In JavaScript this is
 not legal.`

console.log(`string text line 1
string text line 2`);

或者在字符串中嵌入变量。

// 字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`

如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。

let greeting = `\`Yo\` World!`;// `Yo` World!

模板字符串中嵌入变量,需要将变量名写在${}之中。

function authorize(user, action) {
  if (!user.hasPrivilege(action)) {
    throw new Error(
      // 传统写法为
      // 'User '
      // + user.name
      // + ' is not authorized to do '
      // + action
      // + '.'
      `User ${user.name} is not authorized to do ${action}.`);
  }
}
4.标签模板

函数调用的一种特殊形式

let a = 5;
let b = 10;

tag`Hello ${ a + b } world ${ a * b }`; // 模板字符串前面的一个标识名tag,是一个函数
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);

字符串的新增方法

1.String.fromCodePoint()

ES6新增方法,可以识别大于0xFFFF的字符,弥补了ES5的String.fromCharCode()方法的不足。

注意:有多个参数时会合并成一个字符串返回。

String.fromCodePoint(0x20BB7)
// "𠮷"
String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y'
// true
2.String.raw()

处理模板字符串的基本方法,它会将所有变量替换,而且对斜杠进行转义,方便下一步作为字符串来使用。

其代码实现:

String.raw = function (strings, ...values) {
  let output = '';
  let index;
  for (index = 0; index < values.length; index++) {
    output += strings.raw[index] + values[index];
  }

  output += strings.raw[index]
  return output;
}
3.实例方法:codePointAt()

能够正确处理 4 个字节储存的字符,返回一个字符的码点。

let s = '𠮷a'; // 视为三个字符

s.codePointAt(0) // 134071十进制码点(即十六进制的20BB7)
s.codePointAt(1) // 57271

s.codePointAt(2) // 97
4.实例方法:normalize()

将字符的不同表示方法统一为同样的形式,这称为 Unicode 正规化

normalize方法可以接受一个参数来指定normalize的方式,参数的四个可选值如下。

参数可选值表示
NFC“标准等价合成”,返回多个简单字符的合成字符
NFD“标准等价分解”,在标准等价的前提下,返回合成字符分解的多个简单字符
NFKC“兼容等价合成”,返回合成字符
NFKD“兼容等价分解”,在兼容等价的前提下,返回合成字符分解的多个简单字符
'\u004F\u030C'.normalize('NFC').length // 1 ——NFC参数返回字符的合成形式
'\u004F\u030C'.normalize('NFD').length // 2 ——NFD参数返回字符的分解形式
5.实例方法
方法含义
includes()返回布尔值,表示是否找到了参数
startsWith()返回布尔值,表示参数字符串是否在原字符串的头部
endsWith()返回布尔值,表示参数是否在原则符串的尾部。
以上三个方法都支持第二个参数,表示开始搜索位置。
repeat()返回一个新的字符串,表示将原字符串重复n次
参数为小数—取整;为负数或者Infinity—报错
padStart()返回字符串,用于字符串在头部补全长度
padEnd()返回字符串,用于字符串在尾部补全长度
以上两个方法接受两个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串。如果原字符串的长度,等于或大于最大长度,则字符串补全不生效,返回原字符串,若省略第二个参数,默认使用空格补全长度
trimStart()消除字符串头部的空格,返回新字符串,不会修改原始字符串
trimEnd()消除字符串尾部的空格,返回新字符串,不会修改原始字符串
matchAll()返回一个正则表达式在当前字符串的所有匹配
replaceAll()一次性替换所有匹配,返回一个新字符串,不会改变原字符串
String.prototype.replaceAll(searchValue, replacement)
第二个参数replacement表示替换的文本,其中可以使用一些特殊字符串。
at()接受一个整数作为参数,返回参数指定位置的字符,支持负索引(即倒数的位置)。

数值的扩展

1.二进制和八进制表示法

二进制:前缀0b(或0B)

八进制:前缀0o(或0O)

2.数值分隔符

ES2021,允许 JavaScript 的数值使用下划线(_)作为分隔符

注意:

  • 不能放在数值的最前面(leading)或最后面(trailing)。
  • 不能两个或两个以上的分隔符连在一起。
  • 小数点的前后不能有分隔符。
  • 科学计数法里面,表示指数的eE前后不能有分隔符。

Number()、parseInt()、parseFloat()不支持数值分隔符。

3.方法
方法含义
Number.isFinite()返回布尔值,检查一个数值是否为有限的(finite)
Number.isNaN()返回布尔值,检查一个值是否为NaN
Number.isInteger()返回布尔值,用来判断一个数值是否为整数
4.Number.EPSILON

新增一个极小的常量Number.EPSILON。根据规格,它表示 1 与大于 1 的最小浮点数之间的差。

实际上是 JavaScript 能够表示的最小精度,可以用来设置“能够接受的误差范围”

5.Math 对象的扩展
方法名作用
Math.trunc()用于去除一个数的小数部分,返回整数部分
Math.sign()用来判断一个数到底是正数、负数、还是零。返回值:+1、-1、0、-0、NaN
Math.cbrt()用于计算一个数的立方根。
Math.clz32()将参数转为 32 位无符号整数的形式,然后返回这个 32 位值里面有多少个前导 0
Math.imul()返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数
Math.fround()返回一个数的32位单精度浮点数形式
Math.hypot()返回所有参数的平方和的平方根

对数方法:

  • Math.expm1(x)返回 ex - 1
  • Math.log1p(x)方法返回1 + x的自然对数
  • Math.log10(x)返回以 10 为底的x的对数
  • Math.log2(x)返回以 2 为底的x的对数

双曲线函数:

  • Math.sinh(x) 返回x的双曲正弦(hyperbolic sine)
  • Math.cosh(x) 返回x的双曲余弦(hyperbolic cosine)
  • Math.tanh(x) 返回x的双曲正切(hyperbolic tangent)
  • Math.asinh(x) 返回x的反双曲正弦(inverse hyperbolic sine)
  • Math.acosh(x) 返回x的反双曲余弦(inverse hyperbolic cosine)
  • Math.atanh(x) 返回x的反双曲正切(inverse hyperbolic tangent)
6.BigInt函数

可以用它生成 BigInt 类型的数值。转换规则基本与Number()一致,将其他类型的值转为 BigInt。

BigInt 继承了 Object 对象的两个实例方法。

  • BigInt.prototype.toString()
  • BigInt.prototype.valueOf()

继承了 Number 对象的一个实例方法。

  • BigInt.prototype.toLocaleString()

提供了三个静态方法。

  • BigInt.asUintN(width, BigInt): 给定的 BigInt 转为 0 到 2width - 1 之间对应的值。
  • BigInt.asIntN(width, BigInt):给定的 BigInt 转为 -2width - 1 到 2width - 1 - 1 之间对应的值。
  • BigInt.parseInt(string[, radix]):近似于Number.parseInt(),将一个字符串转换成指定进制的 BigInt。

几乎所有的数值运算符都可以用在 BigInt,但是有两个例外。

  • 不带符号的右移位运算符>>>
  • 一元的求正运算符+

函数的扩展

1.函数参数的默认值
  • 基本使用

    ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。

    function log(x, y = 'World') {
      console.log(x, y);
    }
    
    log('Hello') // Hello World
    log('Hello', 'China') // Hello China
    log('Hello', '') // Hello
    
  • 与解构赋值默认值结合使用

    // 写法一
    function m1({x = 0, y = 0} = {}) {
      return [x, y];
    }
    
    // 写法二
    function m2({x, y} = { x: 0, y: 0 }) {
      return [x, y];
    }
    
    // 函数没有参数的情况
    m1() // [0, 0]
    m2() // [0, 0]
    
    // x 和 y 都有值的情况
    m1({x: 3, y: 8}) // [3, 8]
    m2({x: 3, y: 8}) // [3, 8]
    
    // x 有值,y 无值的情况
    m1({x: 3}) // [3, 0]
    m2({x: 3}) // [3, undefined]
    
    // x 和 y 都无值的情况
    m1({}) // [0, 0];
    m2({}) // [undefined, undefined]
    
    m1({z: 3}) // [0, 0]
    m2({z: 3}) // [undefined, undefined]
    
  • 参数默认值的位置

    通常情况下,定义了默认值的参数,应该是函数的尾参数。

    如果非尾部的参数设置默认值,实际上这个参数是没法省略的,无法只省略该参数,而不省略它后面的参数,除非显式输入undefined

    // 例一
    function f(x = 1, y) {
      return [x, y];
    }
    
    f() // [1, undefined]
    f(2) // [2, undefined]
    f(, 1) // 报错
    f(undefined, 1) // [1, 1]
    
    // 例二
    function f(x, y = 5, z) {
      return [x, y, z];
    }
    
    f() // [undefined, 5, undefined]
    f(1) // [1, 5, undefined]
    f(1, ,2) // 报错
    f(1, undefined, 2) // [1, 5, 2]
    
  • 函数的length属性

    指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数

  • 作用域

    设置参数默认值后,函数进行声明初始化时,参数会形成一个单独的作用域(context),等到初始化结束,这个作用域就会消失。

  • 应用

​ 利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误

2.rest参数

rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象

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

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

注意:rest 参数之后不能再有其他参数,会报错

// 报错
function f(a, ...b, c) {
  // ...
}
3.严格模式

ES6中规定了规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。

两种方法可以规避这种限制

  • 第一种是设定全局性的严格模式,这是合法的

    'use strict';
    
    function doSomething(a, b = a) {
      // code
    }
    
  • 第二种是把函数包在一个无参数的立即执行函数里面

    const doSomething = (function () {
      'use strict';
      return function(value = 42) {
        return value;
      };
    }());
    
4.箭头函数
  • 基本用法

    var f = () => 5;
    console.log(f()); // 5
    var sum = (num1, num2) => num1 + num2;
    console.log(sum(1, 2)); // 3
    
    // 箭头函数的代码块部分多于一条语句上,要用大括号将它们括起来并且使用return语句返回
    
    • 箭头函数和变量解构结合使用

       const full = ({ first, last }) => first + " " + last;
       
       // 等同于
       function full(person) {
        return person.first + ' ' + person.last;
      }
      
    • 简化回调函数

      // 普通函数写法
      [1,2,3].map(function (x) {
        return x * x;
      });
      
      // 箭头函数写法
      [1,2,3].map(x => x * x);
      
    • rest参数和箭头函数结合

      const numbers = (...nums) => nums;
      numbers(1, 2, 3, 4, 5) // [1,2,3,4,5]
      
      const headAndTail = (head, ...tail) => [head, tail];
      headAndTail(1, 2, 3, 4, 5); // [1,[2,3,4,5]]
      
  • 注意点:

    • 箭头函数没有自己的this对象。

      内部的this就是定义时上层作用域中的this

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

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

    • 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

除了thisargumentssupernew.target这三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量

  • 不适用场合

    • 定义对象的方法且该方法内部包括this

      const cat = {
        lives: 9,
        jumps: () => {
          this.lives--;
        }
      }
      console.log(cat.jumps()); // undefined 由于对象不构成单独作用域,箭头函数的this指向全局对象
      

      解决方法:对象的属性用传统的写法定义

    • 需要动态this的时候

      var button = document.getElementById('press');
      button.addEventListener('click', () => { // 监听函数是一个箭头函数,里面的this指向全局对象,点击按钮会报错
        this.classList.toggle('on');
      });
      
5.尾调用优化
  • 含义

    尾调用(Tail Call):某个函数的最后一步是调用另一个函数。

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

    尾调用不一定出现在函数尾部,只要是最后一步操作即可。

    function f(x) {
      if (x > 0) {
        return m(x); //m(x)和n(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 addOne(a){
      var one = 1;
      function inner(b){ // 内层函数inner用到了外层函数addOne的内部变量one,不会尾调用优化
        return b + one;
      }
      return inner(a);
    }
    
  • 尾递归

    尾调用自身(递归容易栈溢出,但是尾调由于只存在一个调用帧,所以不存在这个风险)

    function factorial(n) { // 阶乘函数 计算n的阶乘 需要保存n个调用记录 复杂度O(n)
      if (n === 1) return 1;
      return n * factorial(n - 1);
    }
    factorial(5) // 120
    
    function factorial(n, total) { // 尾递归方式 只保留一个调用记录 复杂度O(1)
      if (n === 1) return total;
      return factorial(n - 1, n * total);
    }
    factorial(5, 1) // 120
    

    例子:计算 Fibonacci 数列,验证尾递归优化重要性

    // 非尾递归的 Fibonacci 数列实现
    function Fibonacci (n) {
      if ( n <= 1 ) {return 1};
    
      return Fibonacci(n - 1) + Fibonacci(n - 2);
    }
    
    Fibonacci(10) // 89
    Fibonacci(100) // 超时
    Fibonacci(500) // 超时
    
    // 尾递归优化过的 Fibonacci 数列实现
    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
    
  • 递归函数的改写

    尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身内部。做法:变量改写为函数参数

    以上面阶乘计算为例,存在中间变量total,将其改写为函数参数

    • 方法一:在尾递归函数之外,再提供一个正常形式的函数。

      function tailFactorial(n, total) {
        if (n === 1) return total;
        return tailFactorial(n - 1, n * total);
      }
      
      function factorial(n) { // 正常形式的factorial函数来调用尾递归函数tailFactorial
        return tailFactorial(n, 1);
      }
      
      factorial(5) // 120
      

      使用柯里化(将多参数的函数转换成单参数的形式)

      function currying(fn, n) {
        return function (m) {
          return fn.call(this, m, n);
        };
      }
      
      function tailFactorial(n, total) {
        if (n === 1) return total;
        return tailFactorial(n - 1, n * total);
      }
      
      const factorial = currying(tailFactorial, 1);
      
      factorial(5) // 120
      
    • 方法二:采用ES6的默认值

      function factorial(n, total = 1) { // 参数total有默认值1
        if (n === 1) return total;
        return factorial(n - 1, n * total);
      }
      
      factorial(5) // 120
      
  • 尾调用优化实现

    只在严格模式下开启。正常模式无效——自己实现尾递归优化:采用“循环”换掉“递归”。

    // 正常的递归函数
    function sum(x, y) {
      if (y > 0) {
        return sum(x + 1, y - 1);
      } else {
        return x;
      }
    }
    
    sum(1, 100000) // 报错
    
    function sum(x, y) {
      if (y > 0) {
        return sum.bind(null, x + 1, y - 1);
      } else {
        return x;
      }
    }
    
    trampoline(sum(1, 100000)) // 1000001 使用蹦床函数trampoline不会发生调用栈溢出
    

数组的扩展

1.扩展运算法(spread)(…)
  • 含义

    rest的逆运算,将数组转为用逗号分隔的参数序列

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

    主要用于函数调用

    function add(x, y) {
      return x + y;
    }
    
    const numbers = [4, 38];
    add(...numbers) // 42
    

    可以和正常的函数参数结合使用,扩展运算符后面是一个空数组时不生效

    function f(v, w, x, y, z) { }
    const args = [0, 1];
    f(-1, ...args, 2, ...[3]);
    

    注意:只有函数调用时,扩展运算符才可以放在圆括号中,否则会报错

    (...[1, 2])
    // Uncaught SyntaxError: Unexpected number
    
    console.log((...[1, 2]))
    // Uncaught SyntaxError: Unexpected number
    
    console.log(...[1, 2])
    // 1 2
    
  • 替代函数的apply()

    扩展运算符取代apply()方法的实例,应用Math.max()方法,简化求出一个数组最大元素的写法。

    // ES5 的写法
    Math.max.apply(null, [14, 3, 77])
    
    // ES6 的写法
    Math.max(...[14, 3, 77])
    
    // 等同于
    Math.max(14, 3, 77);
    
  • 扩展运算符的应用

    • 复制数组

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

      const a1 = [1, 2];
      const a2 = a1;
      
      a2[0] = 2;
      a1 // [2, 2] 对a2进行修改会影响a1
      
      // ES5克隆的方法
      const a1 = [1, 2];
      const a2 = a1.concat();
      
      // 扩展运算符提供的方法
      const a1 = [1, 2];
      // 写法一
      const a2 = [...a1];
      // 写法二
      const [...a2] = a1;
      
    • 合并数组

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

      注意:上面两个合并数组的方法都是浅拷贝

    • 与解构赋值结合 用于生成数组

      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   // []
      

      将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。

    • 字符串

      将字符串转为真正的数组

      [...'hello']
      // [ "h", "e", "l", "l", "o" ]
      
    • 实现了 Iterator 接口的对象

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

      let nodeList = document.querySelectorAll('div'); // 返回NodeList对象
      let array = [...nodeList]; // 扩展运算符将其转为真正的数组
      
      let arrayLike = { // 没有部署Iterator接口
        '0': 'a',
        '1': 'b',
        '2': 'c',
        length: 3
      };
      
      // TypeError: Cannot spread non-iterable object.结果报错,可以改用Array.from方法将arrayLike转为真正的数组
      let arr = [...arrayLike];
      
    • Map和Set结构,Generator函数

      只要具有 Iterator 接口的对象,都可以使用扩展运算符

      let map = new Map([
        [1, 'one'],
        [2, 'two'],
        [3, 'three'],
      ]);
      
      let arr = [...map.keys()]; // [1, 2, 3]
      
2.Array.from()
  • 运用对象

    可以将下面两类对象转为真正的数组:

    • 类似数组的对象(array-like object):本质特征只有一点,即必须有length属性

      let arrayLike = {
          '0': 'a',
          '1': 'b',
          '2': 'c',
          length: 3
      };
      
      // ES5 的写法
      var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
      
      // ES6 的写法
      let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
      

      常见的类似数组的对象是 DOM 操作返回的 NodeList 集合,以及函数内部的arguments对象。

    • 可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。

  • 参数是一个真正的数组,Array.from()会返回一个一模一样的新数组

    Array.from([1, 2, 3])
    // [1, 2, 3]
    
  • 对于没有部署Array.from方法的浏览器,可以用Array.prototype.slice()方法替代。

    const toArray = (() =>
      Array.from ? Array.from : obj => [].slice.call(obj)
    )();
    
  • Array.from()还可以接受一个函数作为第二个参数,作用类似于数组的map()方法,用来对每个元素进行处理,将处理后的值放入返回的数组。

    Array.from(arrayLike, x => x * x);
    // 等同于
    Array.from(arrayLike).map(x => x * x);
    
    Array.from([1, 2, 3], (x) => x * x)
    // [1, 4, 9]
    
  • Array.from()的另一个应用是,将字符串转为数组,然后返回字符串的长度。

​ 能正确处理各种 Unicode 字符,可以避免 JavaScript 将大于\uFFFF的 Unicode 字符,算作两个字符的 bug。

3.Array.of()

Array.of()方法用于将一组值转换为数组。返回参数值组成的数组。如果没有参数,就返回一个空数组

代码实现:

function ArrayOf(){
  return [].slice.call(arguments);
}
4.实例方法
1.copyWithin()
方法名含义
copyWithin()在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组
会改变原数组
Array.prototype.copyWithin(target(必选), start = 0, end = this.length)
copyWithin()实例
// 将3号位复制到0号位 
[1, 2, 3, 4, 5].copyWithin(0, 3, 4)
// [4, 2, 3, 4, 5]

// -2相当于3号位,-1相当于4号位
[1, 2, 3, 4, 5].copyWithin(0, -2, -1)
// [4, 2, 3, 4, 5]

// 将3号位复制到0号位(?????)                          
[].copyWithin.call({length: 5, 3: 1}, 0, 3) 
// 普通对象{length: 5, 3: 1} 转成数组为 [undefined, undefined, undefined, 1, undefined]
// {0: 1, 3: 1, length: 5} 转成数组为 [1, undefined, 1, undefined, undefined]

// 将2号位到数组结束,复制到0号位
let i32a = new Int32Array([1, 2, 3, 4, 5]);
i32a.copyWithin(0, 2);
// Int32Array [3, 4, 5, 4, 5]

// 对于没有部署 TypedArray 的 copyWithin 方法的平台
// 需要采用下面的写法
[].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4);
// Int32Array [4, 2, 3, 4, 5]
2.find()\findIndex()\findLast()\findIndexLast()
方法名含义
find()找出第一个符合条件的数组成员。参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。没有符合条件的成员,则返回undefined
find()实例
// 找出数组中第一个小于 0 的成员
[1, 4, -5, 10].find((n) => n < 0)
// -5

// 找出数组中第一个大于 9 的成员
[1, 5, 10, 15].find(function(value, index, arr) { // 回调函数可以接受三个参数:当前值、当前位置、原数组
  return value > 9;
}) // 10

// 方法可以接受第二个参数,用于绑定回调函数的this对象
function f(v){
  return v > this.age;
}
let person = {name: 'John', age: 20};
[10, 12, 26, 15].find(f, person);    // 26
方法名含义
findIndex()返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1
findIndex()实例
[1, 5, 10, 15].findIndex(function(value, index, arr) {
  return value > 9;
}) // 2

find()、findIndex()方法可以发现NAN,弥补了indexOf()的不足

[NaN].indexOf(NaN)
// -1

[NaN].findIndex(y => Object.is(NaN, y)) // 借助Object.is()方法可以做到
// 0

findLast()findLastIndex()从数组结尾开始。

3.fill()
方法名含义
fill()使用给定值,填充一个数组。
实例
['a', 'b', 'c'].fill(7)
// [7, 7, 7]

new Array(3).fill(7)
// [7, 7, 7]

fill方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。

fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置fill(value, start包括, end不包括)

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

注意,赋值类型为对象时,赋值的是同一内存地址的对象,不是深拷贝对象

let arr = new Array(3).fill({name: "Mike"}); // [{name: "Mike"}, {name: "Mike"}, {name: "Mike"}]
arr[0].name = "Ben"; // 只修改第一个对象属性
arr // [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]所有的会改变
4.entries(),keys() 和 values()

用于数组遍历,返回一个遍历器对象,可以用for...of循环遍历。

区别

  • keys():对键名的遍历
  • values()对键值的遍历
  • entries()对键值对的遍历
实例
for (let index of ['a', 'b'].keys()) {
  console.log(index);
}
// 0
// 1

for (let elem of ['a', 'b'].values()) {
  console.log(elem);
}
// 'a'
// 'b'

for (let [index, elem] of ['a', 'b'].entries()) {
  console.log(index, elem);
}
// 0 "a"
// 1 "b"

可以手动调用遍历器对象的next方法进行遍历(不使用for...of的情况)

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()方法

Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值。

实例 
// includes(value,start搜索的起始位置)
[1, 2, 3].includes(3, 3);  // false
[1, 2, 3].includes(3, -1); // true

之前解决是否包含某个值用的是indexOf方法,缺点:不够语义化,内部使用严格相等运算符(===)会导致对NaN的误判。

[NaN].indexOf(NaN)
// -1
[NaN].includes(NaN)
// true

Map 和 Set 数据结构有一个has方法,需要注意与includes区分。

  • Map 结构的has方法,是用来查找键名的,比如Map.prototype.has(key)WeakMap.prototype.has(key)Reflect.has(target, propertyKey)
  • Set 结构的has方法,是用来查找值的,比如Set.prototype.has(value)WeakSet.prototype.has(value)
6.flat(),flatMap() 方法

flat()用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响

实例 flat(number)
[1, 2, [3, [4, 5]]].flat() // number不写默认为1 
// [1, 2, 3, [4, 5]]

[1, 2, [3, [4, 5]]].flat(2) // 写几就拉平几层
// [1, 2, 3, 4, 5]

[1, [2, [3]]].flat(Infinity) // 不管有几层都要拉平用Infinity关键字
// [1, 2, 3]

[1, 2, , 4, 5].flat() // 有空位会跳过空位
// [1, 2, 4, 5]

flatMap()方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。

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

flatMap()只能展开一层数组。

flatMap()方法的参数是一个遍历函数,该函数可以接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组。

arr.flatMap(function callback(currentValue[, index[, array]]) {
  // ...
}[, thisArg])
7.at()

at()方法接受一个整数作为参数,返回对应位置的成员,并支持负索引。这个方法不仅可用于数组,也可用于字符串和类型数组(TypedArray)。

实例
const arr = [5, 12, 8, 130, 44];
arr.at(2) // 8
arr.at(-2) // 130

如果参数位置超出了数组范围,at()返回undefined

实例
const sentence = 'This is a sample sentence';

sentence.at(0); // 'T'
sentence.at(-1); // 'e'

sentence.at(-100) // undefined
sentence.at(100) // undefined
8.toReversed(),toSorted(),toSpliced(),with()

允许对数组进行操作时,不改变原数组,而返回一个原数组的拷贝。

它们分别对应数组的原有方法。

  • toReversed()对应reverse(),用来颠倒数组成员的位置。
  • toSorted()对应sort(),用来对数组成员排序。
  • toSpliced()对应splice(),用来在指定位置,删除指定数量的成员,并插入新成员。
  • with(index, value)对应splice(index, 1, value),用来将指定位置的成员替换为新的值。

上面是这四个新方法对应的原有方法,含义和用法完全一样,唯一不同的是不会改变原数组,而是返回原数组操作后的拷贝。

9.group(),groupToMap()

group()的参数是一个分组函数,原数组的每个成员都会依次执行这个函数,确定自己是哪一个组。

实例
const array = [1, 2, 3, 4, 5];

array.group((num, index, array) => {
  return num % 2 === 0 ? 'even': 'odd';
});
// { odd: [1, 3, 5], even: [2, 4] }

group()的分组函数可以接受三个参数,数组的当前成员、该成员的位置序号、原数组(上mainnum, index, array),返回值应该是字符串(或者可以自动转为字符串),以作为分组后的组名。

group()的返回值是一个对象,该对象的键名就是每一组的组名,即分组函数返回的每一个字符串(上例是evenodd);该对象的键值是一个数组,包括所有产生当前键名的原数组成员。

groupToMap()的作用和用法与group()完全一致,唯一的区别是返回值是一个 Map 结构,而不是对象

总结:按照字符串分组就使用group(),按照对象分组就使用groupToMap()

10.数组的空位

指的是数组的某一位置没有任何值。

注意:空位不是undefined,某一个位置的值等于undefined,依然是有值的。空位是没有任何值

0 in [undefined, undefined, undefined] // true 数组的0号位是有值的
0 in [, , ,] // false 数组的0号位是没有任何值的

ES6 中明确将空位转为undefined

对象的扩展

1.属性的简洁表示法

ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。

const foo = 'bar';
const baz = {foo}; // 属性值就是变量名,属性值就是变量值
baz // {foo: "bar"}

// 等同于
const baz = {foo: foo}; 

方法的简写

const o = {
  method() {
    return "Hello!";
  }
};

// 等同于
const o = {
  method: function() {
    return "Hello!";
  }
};

CommonJS 模块输出一组变量使用简洁写法

let ms = {};

function getItem (key) {
  return key in ms ? ms[key] : null;
}

function setItem (key, value) {
  ms[key] = value;
}

function clear () {
  ms = {};
}

module.exports = { getItem, setItem, clear };
// 等同于
module.exports = {
  getItem: getItem,
  setItem: setItem,
  clear: clear
};

注意:简写的对象方法不能用作构造函数,会报错

const obj = {
  f() {
    this.foo = 'bar';
  }
};
new obj.f() // 报错
2.属性名表达式

JavaScript 定义对象的属性,有两种方法。

// 方法一:直接用标识符作为属性名
obj.foo = true;

// 方法二:用表达式作为属性名,要将表达式放在方括号之内
obj['a' + 'bc'] = 123;

使用字面量方式定义对象(使用大括号)时,ES5只允许使用方法一,ES6可以使用方法二。

let propKey = 'foo';

let obj = {
  [propKey]: true,
  ['a' + 'bc']: 123
};

注意:属性名表达式与简洁表示法,不能同时使用,会报错。

// 报错
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };

// 正确
const foo = 'bar';
const baz = { [foo]: 'abc'};

注意,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串[object Object],这一点要特别小心。

const keyA = {a: 1};
const keyB = {b: 2};

const myObject = {
  [keyA]: 'valueA',
  [keyB]: 'valueB'
};

myObject // Object {[object Object]: "valueB"}
3.方法的name属性

方法的name属性返回函数名(即方法名)

const person = {
  sayName() {
    console.log('hello!');
  },
};
person.sayName.name   // "sayName"

如果对象的方法使用了取值函数(getter)和存值函数(setter),则name属性不是在该方法上面,而是该方法的属性的描述对象的getset属性上面,返回值是方法名前加上getset

const obj = {
  get foo() {},
  set foo(x) {}
};

obj.foo.name
// TypeError: Cannot read property 'name' of undefined

const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');

descriptor.get.name // "get foo"
descriptor.set.name // "set foo"

有两种特殊情况:

bind方法创造的函数,name属性返回bound加上原函数的名字;

var doSomething = function() {
  // ...
};
doSomething.bind().name // "bound doSomething"

Function构造函数创造的函数,name属性返回anonymous

(new Function()).name // "anonymous"

对象的方法是一个 Symbol 值,那么name属性返回的是这个 Symbol 值的描述。

const key1 = Symbol('description');
const key2 = Symbol();
let obj = {
  [key1]() {},
  [key2]() {},
};
obj[key1].name // "[description]" key1对应的 Symbol 值有描述
obj[key2].name // "" key2没有
4.属性的可枚举性和可遍历性
  • 可枚举性

    对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。

    Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。

    let obj = { foo: 123 };
    Object.getOwnPropertyDescriptor(obj, 'foo')
    //  {
    //    value: 123,
    //    writable: true,
    //    enumerable: true, // 可枚举性,属性为false,表示某些操作会忽略当前属性
    //    configurable: true
    //  }
    

    有四个操作会忽略enumerablefalse的属性。

    • for...in循环:只遍历对象自身的和继承的可枚举的属性。
    • Object.keys():返回对象自身的所有可枚举的属性的键名。
    • JSON.stringify():只串行化对象自身的可枚举的属性。
    • Object.assign(): 忽略enumerablefalse的属性,只拷贝对象自身的可枚举的属性。(ES6新增的)

    ES6 规定,所有 Class 的原型的方法都是不可枚举的,操作中引入继承的属性会让问题复杂化,尽量不要用for...in循环,而用Object.keys()代替

  • 属性的遍历

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

    (1)for…in

    for...in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。

    (2)Object.keys(obj)

    Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。

    (3)Object.getOwnPropertyNames(obj)

    Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。

    (4)Object.getOwnPropertySymbols(obj)

    Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名。

    (5)Reflect.ownKeys(obj)

    Reflect.ownKeys返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

    上面5种方法都遵守属性遍历的次序规则。

    • 首先遍历所有数值键,按照数值升序排列。

    • 其次遍历所有字符串键,按照加入时间升序排列。

    • 最后遍历所有 Symbol 键,按照加入时间升序排列。

      Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
      // ['2', '10', 'b', 'a', Symbol()]
      
5.super关键字

关键字super,指向当前对象的原型对象

const proto = {
  foo: 'hello'
};

const obj = {
  foo: 'world',
  find() {
    return super.foo;
  }
};

Object.setPrototypeOf(obj, proto); // 指定了obj的原型对象为proto
obj.find() // "hello"

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

6.对象的扩展运算符
  • 解构赋值

    实例
    let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
    x // 1
    y // 2
    z // { a: 3, b: 4 } 解构值所在的对象,相当于将等号右边的所有尚未读取的键(a和b),将它们连同值一起拷贝过来
    

    如果等号右边是undefinednull,就会报错,因为它们无法转为对象。

    let { ...z } = null; // 运行时错误
    let { ...z } = undefined; // 运行时错误
    

    解构赋值必须是最后一个参数,否则会报错。

    let { ...x, y, z } = someObject; // 句法错误
    let { x, ...y, ...z } = someObject; // 句法错误
    

    注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用

    let obj = { a: { b: 1 } };
    let { ...x } = obj;
    obj.a.b = 2; // 修改这个对象的值,会影响到解构赋值对它的引用
    x.a.b // 2
    

    扩展运算符的解构赋值,不能复制继承自原型对象的属性

    let o1 = { a: 1 };
    let o2 = { b: 2 };
    o2.__proto__ = o1;
    let { ...o3 } = o2;
    o3 // { b: 2 } o3只复制了o2自身的属性,没有复制它的原型对象o1的属性
    o3.a // undefined
    
  • 扩展运算符

    对象的扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。

    对象的扩展运算符,只会返回参数对象自身的、可枚举的属性,尤其是用于类的实例对象时。

    class C {
      p = 12;
      m() {}
    }
    
    let c = new C(); // c是C类的实例对象,对其扩展时,只会返回c自身的属性c.p,不会返回定义在C的原型对象的c的方法c.m()
    let clone = { ...c };
    
    clone.p; // ok
    clone.m(); // 报错
    

    对象的扩展运算符等同于使用Object.assign()方法。

    let aClone = { ...a }; // 只拷贝了对象实例的属性
    // 等同于
    let aClone = Object.assign({}, a);
    
    完整克隆对象(包括拷贝原型的属性)的写法:
    // 写法一
    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)
    )
    

    扩展运算符可以用于合并两个对象。

    let ab = { ...a, ...b };
    // 等同于
    let ab = Object.assign({}, a, b);
    

    如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉,可用于修改现有对象部分的属性。

    let newVersion = { // newVersion对象自定义了name属性,其他属性全部复制自previousVersion对象。
      ...previousVersion,
      name: 'New Name' // Override the name property
    };
    

    如果把自定义属性放在扩展运算符前面,就变成了设置新对象的默认属性值。

    let aWithDefaults = { x: 1, y: 2, ...a };
    // 等同于
    let aWithDefaults = Object.assign({}, { x: 1, y: 2 }, a);
    // 等同于
    let aWithDefaults = Object.assign({ x: 1, y: 2 }, a);
    

    扩展运算符的参数对象之中,如果有取值函数get,这个函数是会执行的。

    let a = {
      get x() {
        throw new Error('not throw yet');
      }
    }
    let aWithXGetter = { ...a }; // 报错
    
7.AggregateError 错误对象

AggregateError 在一个错误对象里面,封装了多个错误。

AggregateError(errors[, message])

两个参数:

  • errors:数组,它的每个成员都是一个错误对象。该参数是必须的。
  • message:字符串,表示 AggregateError 抛出时的提示信息。该参数是可选的。

AggregateError的实例对象有三个属性。

  • name:错误名称,默认为“AggregateError”。

  • message:错误的提示信息。

  • errors:数组,每个成员都是一个错误对象。

    实例
    try {
      throw new AggregateError([
        new Error("some error"),
      ], 'Hello');
    } catch (e) {
      console.log(e instanceof AggregateError); // true
      console.log(e.message);                   // "Hello"
      console.log(e.name);                      // "AggregateError"
      console.log(e.errors);                    // [ Error: "some error" ]
    }
    
8.Error 对象的 cause 属性

Error 对象用来表示代码运行时的异常情况,添加了一个cause属性,可以在生成错误时,添加报错原因的描述。

用法:new Error()生成 Error 实例时,给出一个描述对象,该对象可以设置cause属性。

const actual = new Error('an error!', { cause: 'Error cause' }); // cause属性,写入报错的原因
actual.cause; // 'Error cause' 可以从实例对象上读取这个属性

运算符的扩展

1.指数运算符

指数运算符(**

2 ** 2 // 4
2 ** 3 // 8

特点:右结合,多个指数运算符连用时,是从最右边开始计算的。

// 相当于 2 ** (3 ** 2)
2 ** 3 ** 2
// 512

指数运算符可以与等号结合,形成一个新的赋值运算符(**=

let a = 1.5;
a **= 2;
// 等同于 a = a * a;

let b = 4;
b **= 3;
// 等同于 b = b * b * b;
2.链判断运算符

链判断运算符(?.)

// 读取message.body.user.firstName这个属性,安全的写法
const firstName = (message
  && message.body
  && message.body.user
  && message.body.user.firstName) || 'default';

// ?.运算符,直接在链式调用的时候判断,左侧的对象是否为null或undefined。如果是的,就不再往下运算,而是返回undefined
const firstName = message?.body?.user?.firstName || 'default';

判断对象方法是否存在,如果存在就立即执行的例子。

iterator.return?.()
// iterator.return如果有定义,就会调用该方法,否则iterator.return直接返回undefined,不再执行?.后面的部分
  • 链判断运算符?.有三种写法

    • obj?.prop // 对象属性是否存在

    • obj?.[expr] // 同上

    • func?.(...args) // 函数或对象方法是否存在

  • ?.运算符常见形式,以及不使用该运算符时的等价形式:

a?.b
// 等同于
a == null ? undefined : a.b

a?.[x]
// 等同于
a == null ? undefined : a[x]

a?.b()
// 等同于
a == null ? undefined : a.b() 

a?.()
// 等同于
a == null ? undefined : a()
  • 注意点

    • 短路机制

      a?.[++x]
      // 等同于
      a == null ? undefined : a[++x] // a是undefined或null,x不会进行递增运算
      

      链判断运算符一旦为真,右侧的表达式就不再求值。

    • 括号的影响

      属性链有圆括号,链判断运算符对圆括号外部没有影响,只对圆括号内部有影响。

      (a?.b).c
      // 等价于
      (a == null ? undefined : a.b).c
      // ?.对圆括号外部没有影响,不管a对象是否存在,圆括号后面的.c总是会执行。
      
    • 报错场合

      以下写法是禁止的,会报错。

      // 构造函数
      new a?.()
      new a?.b()
      
      // 链判断运算符的右侧有模板字符串
      a?.`{b}`
      a?.b`{c}`
      
      // 链判断运算符的左侧是 super
      super?.()
      super?.foo
      
      // 链运算符用于赋值运算符左侧
      a?.b = c
      
    • 右侧不得为十进制数值

      规定如果?.后面紧跟一个十进制数字,那么?.不再被看成是一个完整的运算符,会按照三元运算符进行处理

      foo?.3:0
      // 会被解析成
      foo ? .3 : 0
      
3.Null判断运算符

常见读取对象属性,判断属性值是null或undefined时指定默认值,是通过||运算符指定默认值,但是属性的值如果为空字符串或false0,默认值也会生效。

Null 判断运算符(??):作用类似||,但是只有运算符左侧的值为nullundefined,才会返回右侧的值。

const headerText = response.settings.headerText ?? 'Hello, world!';
const animationDuration = response.settings.animationDuration ?? 300;
const showSplashScreen = response.settings.showSplashScreen ?? true;

用法:

  • 和链判断运算符?.配合使用,为nullundefined的值设置默认值。

    const animationDuration = response.settings?.animationDuration ?? 300;
    // 如果response.settings是null或undefined
    // 或者response.settings.animationDuration是null或undefined
    // 返回默认值300。
    
  • 判断函数参数是否赋值。

    function Component(props) {
      const enable = props.enabled ?? true // 判断props参数的enabled属性是否赋值
      // …
    }
    
    //等同于
    function Component(props) {
      const {
        enabled: enable = true,
      } = props;
      // …
    }
    

注意:??本质是逻辑运算符,和|| &&一起使用时,需要用括号表明优先级。

// 报错
lhs && middle ?? rhs
lhs || middle ?? rhs

// 正确写法
(lhs && middle) ?? rhs;
lhs && (middle ?? rhs);

(lhs || middle) ?? rhs;
lhs || (middle ?? rhs);
4.赋值运算符

或赋值运算符||= 、与赋值运算符&&= 、Null 赋值运算符??=

// 或赋值运算符
x ||= y
// 等同于
x || (x = y)

// 与赋值运算符
x &&= y
// 等同于
x && (x = y)

// Null 赋值运算符
x ??= y
// 等同于
x ?? (x = y)

三个运算符||=&&=??=相当于先进行逻辑运算,然后根据运算结果,再视情况进行赋值运算。

用途:为变量或属性设置默认值

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

// 新的写法
user.id ||= 1;
// 参数对象opts如果不存在属性foo和属性baz,则为这两个属性设置默认值。
function example(opts) {
  opts.foo = opts.foo ?? 'bar';
  opts.baz ?? (opts.baz = 'qux');
}
// 有了“Null 赋值运算符”以后,就可以统一写成下面这样
function example(opts) {
  opts.foo ??= 'bar';
  opts.baz ??= 'qux';
}

Promise对象

1.Promise的含义

Promise 是异步编程的一种解决方案,它作为一个容器(对象),里面包裹着某个未来才会结束的事件(一个异步操作)。

Promise对象的特点:

  • 对象的状态不受外界影响。只有异步操作的结果可以决定当前是哪一种状态。

    异步操作有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。

  • 一旦状态改变,就不会再变,任何时候都可以得到这个结果。

    Promise对象的状态改变,只有两种可能:

    • pending变为fulfilled
    • pending变为rejected

    只要以上情况发生,结果就不会再发生改变,并且如果此时对Promise对象添加回调函数也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

Promise对象的优点:

  • 将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数
  • 提供统一的接口,使得控制异步操作更加容易

Promise对象的缺点:

  • 无法取消Promise,一旦新建它就会立即执行,无法中途取消
  • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部
  • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)
2.基本用法

Promise对象是一个构造函数,用来生成Promise实例

// Promise实例
const promise = new Promise(function(resolve, reject){ 
  // ...some code
  
  if(/* 异步操作成功 */) {
    resolve(value);
  } else {
    reject(error);
  }
})

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject

resolve函数作用:将Promise对象的状态从“未完成”变为“成功”,在异步操作成功时调用,将异步结果作为参数传递出去

reject函数的作用:将Promise对象的状态从“未完成”变为“失败”,,在异步操作失败时调用,将异步操作报出的错误作为参数传递出去

// Promise实例生成后,用then方法指定resolve状态和reject状态的回调函数
promise.then(function(value) {
  //success
}, function(error) {
  
});

Promise对象实例

function timeout(ms) { // timeout方法返回一个Promise实例
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 'done'); // 表明过了一段时间(ms参数),会发生的事件
  });
}

timeout(100).then((value) => { // 过了(ms参数),状态变为resolved,触发then方法绑定的回调函数
  console.log(value);
});

Promise 新建后就会立即执行

let promise = new Promise(function(resolve, reject) { // Promise新建后立即执行
  console.log('Promise');
  resolve();
});

promise.then(function() { // then方法指定的回调函数,在当前脚本所有同步任务执行完后才会执行
  console.log('resolved'); // resolved最后输出
});

console.log('Hi!');

// 下面是代码执行顺序
// Promise
// Hi!
// resolved

异步加载图片的例子

function loadImageAsync(url) { // 使用Promise包装了一个图片加载的异步操作
  return new Promise(function(resolve, reject) {
    const image = new Image();
    
    image.onload = function() { // 加载成功,调用resolve方法
      resolve(image);
    };
    
    image.onerror = function() { // 加栽失败,调用reject方法
      reject(new Error('Could not load image at ' + url));
    };
    
    image.src = url;
  })
}

Promise对象实现的 Ajax 操作的例子

const getJSON = function(url) { // getJSON是对 XMLHttpRequest 对象的封装
  const promise = new Promise(function(resolve, reject){
    const handler = function() { // resolve函数和reject函数调用时需要带有参数
      if (this.readyState !== 4) {
        return;
      }
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText)); // Error对象的实例,抛出错误
      }
    };
    const client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.responseType = "json";
    client.setRequestHeader("Accept", "application/json");
    client.send();

  });

  return promise;// 发出一个针对 JSON 数据的 HTTP 请求,并且返回一个Promise对象。
};

getJSON("/posts.json").then(function(json) {
  console.log('Contents: ' + json);
}, function(error) {
  console.error('出错了', error);
});

参数为另外一个Promise实例的例子

const p1 = new Promise(function (resolve, reject) {
  setTimeout(() => reject(new Error('fail')), 3000) // p1是一个 Promise,3 秒之后变为rejected
})

const p2 = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(p1), 1000) // p2的状态在1秒内返回,但是返回的是另一个Promise,所以p2状态无效,由p1的状态决定
})

p2
  .then(result => console.log(result)) // then方法针对的是p1,状态为rejected,触发catch方法指定的回调函数
  .catch(error => console.log(error))
// Error: fail

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

new Promise((resolve, reject) => {
  resolve(1); // 一般调用resolve或reject以后,Promise 的使命就完成了,最好写成return resolve(1);后续放在then方法里
  console.log(2); 
}).then(r => {
  console.log(r);
});
// 2
// 1  //后打印原因:Promise在事件循环的末尾执行,晚于本轮循环的同步任务
3.Promise.prototype.then() ?????接收参数个数

then方法是定义在原型对象Promise.prototype上的。

作用:为Promise实例添加状态改变时的回调函数。

then方法返回的是一个新的Promise实例,可以采用链式写法。

// 链式写法:then方法后面再调用另一个then方法
getJSON("/posts.json").then(function(json) {
  return json.post; // 第一个回调函数完成后将结果作为参数传入第二个回调函数
}).then(function(post) {
  // ...
});

// 链式写法可以指定一组按照次序调用的回调函数 ?????
getJSON("/post/1.json").then(function(post) { // 第一个then方法指定一个回调函数,返回一个新的Promise对象
  return getJSON(post.commentURL);
}).then(function (comments) { // 第二个then方法根据新的Promise的状态调用相应的回调函数
  console.log("resolved: ", comments);
}, function (err){
  console.log("rejected: ", err);
});
// 上面代码运用箭头函数的简洁写法
getJSON("/post/1.json").then(
	post => getJSON(post.commentURL)
).then(
	comments => console.log("resolved: ", comments),
  err => console.log("rejected: ", err)
);
4.Promise.prototype.catch()

作用:指定发生错误时的回调函数。

getJSON('/posts.json') // getJSON()方法返回一个Promise对象
  .then(function(posts) { 
  // ...
}).catch(function(error) {
  // 处理 getJSON 和 前一个回调函数运行时发生的错误
  console.log('发生错误!', error);
});

promise抛出错误,就会被catch()方法指定的回调函数捕获,reject()方法的作用,等同于抛出错误

// 写法一
const promise = new Promise(function(resolve, reject) {
  try {
    throw new Error('test'); // 抛出错误
  } catch(e) {
    reject(e);
  }
});
promise.catch(function(error) {
  console.log(error);
});

// 写法二
const promise = new Promise(function(resolve, reject) {
  reject(new Error('test')); // reject()方法
});
promise.catch(function(error) {
  console.log(error);
});

注意:如果 Promise 状态已经变成resolved,再抛出错误是无效的。

Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。

getJSON('/post/1.json').then(function(post) {
  return getJSON(post.commentURL);
}).then(function(comments) {
  // some code
}).catch(function(error) {
  // 处理前面三个Promise产生的错误(一个由getJSON()产生,两个由then()产生)
});

一般来说,不要在then()方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。

// bad
promise
  .then(function(data) {
    // success
  }, function(err) {
    // error
  });

// good 尽量用这种写法
promise
  .then(function(data) { //cb
    // success
  })
  .catch(function(err) {
    // error
  });

如果没有使用catch()方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行会报错,因为x没有声明  但是不会退出进程、终止脚本执行
    resolve(x + 2);
  });
};

someAsyncThing().then(function() {
  console.log('everything is great');
});

setTimeout(() => { console.log(123) }, 2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123  可以正常执行,不会受到Promise错误影响
5.Promise.prototype.finally()

finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。

// 实例
server.listen(port) // 服务器使用Promise处理请求
  .then(function () {
    // ...
  })
  .finally(server.stop); // 用finally方法关闭请求
// finally方法的回调函数不接受任何参数,与状态无关的,不依赖于 Promise 的执行结果

finally本质上是then方法的特例

promise
.finally(() => {
  // 语句
});

// 等同于
promise // 不使用finally方法,同样的语句需要为成功和失败两种情况各写一次。有了finally方法,则只需要写一次
.then(
  result => {
    // 语句
    return result;
  },
  error => {
    // 语句
    throw error;
  }
);

finally的实现

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 })
  );
};
// finally方法总是会返回原来的值
6.Promise.all()

作用Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例

const p = Promise.all([p1, p2, p3]); 

Promise.all()方法接受数组作为参数,参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。

p的状态由p1p2p3决定,分成两种情况。

(1)只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected此时第一个被reject的实例的返回值,会传递给p的回调函数。

注意,如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()catch方法。

区别下面两种情况:

const p1 = new Promise((resolve, reject) => { // p1会resolved
  resolve('hello');
})
.then(result => result)
.catch(e => e);

const p2 = new Promise((resolve, reject) => { 
  throw new Error('报错了'); // p2首先会rejected
})
.then(result => result)
.catch(e => e); // catch方法返回一个新的实例,是p2的实际指向,完成catch方法后会变成resolved

Promise.all([p1, p2]) // promise.all()中两个参数都resolved
.then(result => console.log(result)) // 调用then方法指定的回调函数
.catch(e => console.log(e));
// ["hello", Error: 报错了]
// p2没有自己的catch方法,就会调用Promise.all()的catch方法
const p1 = new Promise((resolve, reject) => {
  resolve('hello');
})
.then(result => result);

const p2 = new Promise((resolve, reject) => {
  throw new Error('报错了');
})
.then(result => result);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// Error: 报错了
7.Promise.race()

作用Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例

const p = Promise.race([p1, p2, p3]);

只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

// 实例
const p = Promise.race([
  fetch('/resource-that-may-take-a-while'),
  new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('request timeout')), 5000)
  })
]);

p
.then(console.log)
.catch(console.error);

如果 5 秒之内fetch方法无法返回结果,变量p的状态就会变为rejected,从而触发catch方法指定的回调函数。

8.Promise.allSettled()

用法:用来确定一组异步操作是否都结束了(不管成功或失败)

Promise.allSettled()方法接受一个数组作为参数,数组的每个成员都是一个 Promise 对象,并返回一个新的 Promise 对象。

只有等到参数数组的所有 Promise 对象都发生状态变更(不管是fulfilled还是rejected),返回的 Promise 对象才会发生状态变更。

const promises = [ // 包含三个请求
  fetch('/api-1'),
  fetch('/api-2'),
  fetch('/api-3'),
];

await Promise.allSettled(promises); // 只有三个请求都结束了了,下面的removeLoadingIndicator才会执行
removeLoadingIndicator();

Promise.allSettled()方法返回的新的 Promise 实例,一旦发生状态变更,状态总是fulfilled,不会变成rejected

const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);

const allSettledPromise = Promise.allSettled([resolved, rejected]); // allSettledPromise是方法的返回值

allSettledPromise.then(function (results) {
  console.log(results);
});
// 状态变成fulfilled后,它的回调函数会接收到一个数组作为参数,该数组的每个成员对应前面数组的每个 Promise 对象。
// [
//    { status: 'fulfilled', value: 42 },
//    { status: 'rejected', reason: -1 }
// ]
9.Promise.any()

该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。

Promise.any([
  fetch('https://v8.dev/').then(() => 'home'),
  fetch('https://v8.dev/blog').then(() => 'blog'),
  fetch('https://v8.dev/docs').then(() => 'docs')
]).then((first) => {  // 只要有一个 fetch() 请求成功
  console.log(first);
}).catch((error) => { // 所有三个 fetch() 全部请求失败
  console.log(error);
});

Promise.any()Promise.race()方法很像,只有一点不同,就是Promise.any()不会因为某个 Promise 变成rejected状态而结束,必须等到所有参数 Promise 变成rejected状态才会结束。

10.Promise.resolve()

作用:将现有对象转为 Promise 对象

Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))

Promise.resolve()方法参数的四种情况:

  • 参数是一个Promise实例:不做任何修改、原封不动地返回这个实例

  • 参数是一个thenable对象

    thenable对象指的是具有then方法的对象

    let thenable = { // thenable对象
      then: function(resolve, reject) { 
        resolve(42);
      }
    };
    
    let p1 = Promise.resolve(thenable); // thenable对象里的then方法执行后,对象变为resolved
    p1.then(function (value) { // 然后执行then()方法指定的回调函数
      console.log(value);  // 输出42
    });
    
  • 参数不是具有then()方法的对象,或根本就不是对象

    Promise.resolve()方法返回一个新的 Promise 对象,状态为resolved

    const p = Promise.resolve('Hello'); // 字符串Hello不属于异步操作,返回 Promise 实例的状态从一生成就是resolved
    p.then(function (s) { // 回调函数会立即执行
      console.log(s)
    });
    // Hello
    
  • 不带有任何参数

    Promise.resolve()方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象

    const p = Promise.resolve();
    p.then(function () {
      // ...
    });
    

    注意:立即resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。

    setTimeout(function () {
      console.log('three');
    }, 0); // 在下一轮“事件循环”开始时执行
    
    Promise.resolve().then(function () {
      console.log('two');
    }); // 在本轮“事件循环”结束时执行
    
    console.log('one'); // 立即执行,最先输出
    
    // one
    // two
    // three
    
11.Promise.reject()

返回一个新的 Promise 实例,该实例的状态为rejected

Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数

Promise.reject('出错了')
.catch(e => {
  console.log(e === '出错了')
})
// true
12.应用

1)加载图片

将图片的加载写成一个Promise,一旦加载完成,Promise的状态就发生变化。

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

2) Generator 函数与 Promise 的结合

使用 Generator 函数管理流程,遇到异步操作的时候,通常返回一个Promise对象

function getFoo () {
  return new Promise(function (resolve, reject){ // 2.返回一个promise对象
    resolve('foo');
  });
}

const g = function* () {
  try {
    const foo = yield getFoo(); // 1.异步操作getFoo
    console.log(foo);
  } catch (e) {
    console.log(e);
  }
};

function run (generator) { // 3.处理promise对象并调用一个next方法
  const it = generator();

  function go(result) {
    if (result.done) return result.value;

    return result.value.then(function (value) {
      return go(it.next(value));
    }, function (error) {
      return go(it.throw(error));
    });
  }

  go(it.next());
}

run(g);
13.Promise.try()

作用:让同步函数同步执行,异步函数异步执行,并且让它们具有统一的 API

Promise.try(() => database.users.get({id: userId}))
  .then(...)
  .catch(...)

async 函数

1.含义

Generator 函数的语法糖,使得异步操作变得更加方便。

const fs = require('fs');

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};

// Generator 函数,依次读取两个文件
const gen = function* () {
  const f1 = yield readFile('/etc/fstab');
  const f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

// async函数写法
const asyncReadFile = async function () { // 改变一:Generator 函数的星号(*)替换成async
  const f1 = await readFile('/etc/fstab'); // 改变二:yield替换成await
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

async函数对 Generator 函数的改进:

1)内置执行器

async函数自带执行器,不需要调用next方法或者用co模块,函数执行与普通函数一模一样。

2)更好的语义

asyncawait,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果

3)更广的适应性

async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)

4)返回值是Promise

async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖

2.基本用法

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。

当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

使用形式:

// 函数声明
async function foo() {};

// 函数表达式
const foo = async function() {};

// 对象的方法
let obj = { async foo() {}};
obj.foo().then(...)
               
// Class的方法
class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jake').then();
               
// 箭头函数
const foo = async () => {}; 
3.语法

async函数的语法规则总体上比较简单,难点是错误处理机制。

1)返回Promise对象

async函数返回一个 Promise 对象

async function f() {
  return 'hello world'; // async函数内部的return返回值就是then方法回调函数的参数
}

f().then(v => console.log(v))
// "hello world"

async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。

async function f() {
  throw new Error('出错了'); // 函数内部抛出错误
}

f().then(
  v => console.log('resolve', v),
  e => console.log('reject', e) // 执行reject状态
)
//reject Error: 出错了
2)Promise 对象的状态变化

只有async函数内部的异步操作执行完(内部所有await命令后面的 Promise 对象执行完),才会执行then方法指定的回调函数,除非遇到return语句或者抛出错误。

async function getTitle(url) { // 函数getTitle内部三个操作全部完成才能执行then方法里面的操作
  let response = await fetch(url); // 抓取网页
  let html = await response.text(); // 取出文本
  return html.match(/<title>([\s\S]+)<\/title>/i)[1]; //匹配页面标题
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"
3)await命令

await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。

async function f() {
  await Promise.reject('出错了');
  await Promise.resolve('hello world'); // 不会执行
}

如果需要实现异步操作不中断,将第一个await放在try...catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行。

async function f() {
  try {
    await Promise.reject('出错了');
  } catch(e) {
  }
  return await Promise.resolve('hello world');
}

f()
.then(v => console.log(v))
// hello world

// 方法二:await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误。
async function f() {
  await Promise.reject('出错了')
    .catch(e => console.log(e));
  return await Promise.resolve('hello world');
}

f()
.then(v => console.log(v))
// 出错了
// hello world
4)错误处理。 ?????带try…catch的执行效率前/后

如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject

防止出错的方法,也是将其放在try...catch代码块之中

async function f() {
  try {
    await new Promise(function (resolve, reject) {
      throw new Error('出错了');
    });
  } catch(e) {
  }
  return await('hello world');
}
5)使用注意点
  • await命令放在try...catch代码块中

  • 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发

    // 继发关系 只有getFoo完成后才能触发getBar 比较耗时
    let foo = await getFoo();
    let bar = await getBar();
    
    //同时触发的写法
    // 写法一
    let [foo, bar] = await Promise.all([getFoo(), getBar()]);
    
    // 写法二
    let fooPromise = getFoo();
    let barPromise = getBar();
    let foo = await fooPromise;
    let bar = await barPromise;
    
  • await命令只能用在async函数之中,如果用在普通函数,就会报错

  • async 函数可以保留运行堆栈

4.async 函数的实现原理

实现原理:就是将 Generator 函数和自动执行器,包装在一个函数里。

async function fn(args) {
  // ...
}

// 等同于

function fn(args) {
  return spawn(function* () { // spawn函数:自动执行器
    // ...
  });
}
5.实例:按顺序完成异步操作

实际开发中,经常遇到一组异步操作,需要按照顺序完成。

// async实现
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url); // 是继发请求
    console.log(await response.text());
  }
}
// async实现-并发实现
async function logInOrder(urls) {
  // 并发读取远程URL
  const textPromises = urls.map(async url => { // map方法参数是async函数,但它是并发执行的
    const response = await fetch(url); // 只有async内部是继发执行,外部不受影响
    return response.text();
  });

  // 按次序输出
  for (const textPromise of textPromises) { // for..of循环内部使用了await,实现了按顺序输出
    console.log(await textPromise);
  }
}
6.顶层await

ES2022 开始,允许在模块的顶层独立使用await命令,使得上面那行代码不会报错了。

目的:使用await解决模块异步加载的问题。

// awaiting.js
const dynamic = import(someMission);
const data = fetch(url);
export const output = someProcess((await dynamic).default, await data
                                  
// 加载这个模块的写法
// usage.js
import { output } from "./awaiting.js";
function outputPlusValue(value) { return output + value }

console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100)), 1000);

注意:顶层await只能用在 ES6 模块,不能用在 CommonJS 模块,CommonJS 模块的require()是同步加载,如果有顶层await,就没法处理加载了。

顶层await的一些使用场景

// import() 方法加载
const strings = await import(`/i18n/${navigator.language}`);

// 数据库操作
const connection = await dbConnector();

// 依赖回滚
let jQuery;
try {
  jQuery = await import('https://cdn-a.com/jQuery');
} catch {
  jQuery = await import('https://cdn-b.com/jQuery');
}

Class的基本语法

1.含义

class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法,可以看成一个语法糖。

// ES5写法
function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);

// ES6的class改写
class Point { // 定义了一个类
  constructor(x, y) { // 构造方法:constructor()方法 
    this.x = x; // this关键字代表实例对象
    this.y = y;
  }
  
  toString() { // 定义了toString()方法,前面不需要加function关键字 // 注意:方法与方法之间不用加逗号,会报错!
    return '(' + this.x + ', ' + this.y + ')';
  }
}

// ES6的类可以看作是构造函数的另一种写法
typeOf Point // "function" 类的数据类型是函数
Point === Point.prototype.constructor // true 类本身就指向构造函数

使用与构造函数一样,直接对类使用new命令

class Bar {
  doStuff() {
    console.log('stuff');
  }
}

const b = new Bar();
b.doStuff() // "stuff"

类的所有方法都定义在类的prototype属性上面,

class Point {
  constructor() {
    // ...
  }
  toString() {
    // ...
  }
  toValue() {
    // ...
  }
}

// 等同于
Point.prototype = {
  constructor() {},
  toString() {},
  toValue() {},
};

因此在类的实例上面调用方法,其实就是调用原型上的方法。Object.assign()方法可以很方便地一次向类添加多个方法

class Point {
  constructor(){
    // ...
  }
}

Object.assign(Point.prototype, { // prototype对象的constructor()属性,直接指向“类”的本身
  toString(){},
  toValue(){}
});

注意:类的内部所有定义的方法,都是不可枚举的

class Point {
  constructor(x, y) {
    // ...
  }

  toString() {
    // ...
  }
}

Object.keys(Point.prototype)
// [] toString()方法是Point类内部定义的方法,它是不可枚举的
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
2.constructor()方法

类的默认方法,通过new命令生成对象实例时自动调用该方法。

注意:一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加

class Point {
}

// 等同于
class Point {
  constructor() {}
}

constructor()方法默认返回实例对象(this),也可以指定返回另外一个对象。

class Foo {
  constructor() {
    return Object.create(null); // constructor()函数返回一个全新的对象
  }
}

new Foo() instanceof Foo // 实例对象不是Foo类的实例 
// false
3.类的实例

类必须使用new调用(和普通函数的区别)

class Point {
  // ...
}

// 报错
var point = Point(2, 3);

// 正确
var point = new Point(2, 3);

类的属性和方法,除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

var point = new Point(2, 3);

point.toString() // (2, 3)

上面代码打印point可以得到[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SuChcuz8-1661312481395)(/Users/hb22132/Library/Application Support/typora-user-images/截屏2022-07-14 上午11.28.05.png)]

xy都是实例对象point自身的属性(因为定义在this对象上)

toString()是原型对象的属性(因为定义在Point类上)


类的所有实例共享一个原型对象

var p1 = new Point(2,3);
var p2 = new Point(3,2);

p1.__proto__ === p2.__proto__ // 它们两个的原型都是Point.prototype,所以__proto__属性相等
//true

生产环境中,避免使用__proto__为“类”添加方法,可以使用 Object.getPrototypeOf() 方法来获取实例对象的原型,然后再来为原型添加方法/属性。

4.实例属性的新写法

实例属性除了可以定义在constructor()方法里面的this上面,也可以定义在类内部的最顶层

// 原来的写法
class IncreasingCounter {
  constructor() {
    this._count = 0;
  }
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}

// 新写法
class IncreasingCounter {
  _count = 0; // 直接定义在类的最顶层
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}

注意:新写法定义的属性是 ,而不是定义在实例对象的原型上面。

5.取值函数(getter)和存值函数(setter)

在“类”的内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

class MyClass {
  constructor() {
    // ...
  }
  get prop() { // prop属性设置对应的存值函数
    return 'getter';
  }
  set prop(value) { // prop属性设置对应的取值函数
    console.log('setter: '+value);
  }
}

let inst = new MyClass();

inst.prop = 123;
// setter: 123

inst.prop
// 'getter'

存值函数和取值函数是设置在属性的 Descriptor 对象上的。

6.属性表达式

类的属性名,可以采用表达式。

let methodName = 'getArea';

class Square {
  constructor(length) {
    // ...
  }

  [methodName]() {
    // ...
  }
}

上面代码中,Square类的方法名getArea,是从表达式得到的。

7.Class 表达式

与函数一样,类也可以使用表达式的形式定义。

const MyClass = class Me { // 定义了一个类,名字是Me
  getClassName() {
    return Me.name; // Me在Class的内部可用,指代当前类
  }
};

Me只在 Class 内部有定义。

let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined 在Class外部,这个类只能用MyClass引用。

如果类的内部没用到的话,可以省略Me,也就是可以写成下面的形式。

const MyClass = class { /* ... */ };

采用 Class 表达式,可以写出立即执行的 Class。

let person = new class { // person是一个立即执行的类的实例
  constructor(name) {
    this.name = name;
  }

  sayName() {
    console.log(this.name);
  }
}('张三');

person.sayName(); // "张三"
8.静态方法

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。

静态方法:加上static关键字,就表示该方法不会被实例继承,直接通过类来调用

class Foo {
  static classMethod() { // 静态方法,可以直接在Foo类上调用 Foo.classMethod() // 'hello'
    return 'hello';
  }
}

var foo = new Foo();
foo.classMethod() // 不能在实例上调用
// TypeError: foo.classMethod is not a function

注意,如果静态方法包含this关键字,这个this指的是类,而不是实例。

class Foo {
  static bar() {
    this.baz(); // this指的是Foo类,不是Foo实例,等同于调用 Foo.baz
  }
  static baz() {
    console.log('hello');
  }
  baz() {
    console.log('world'); // 静态方法可以与非静态方法重名,重名不覆盖
  }
}

Foo.bar() // hello

父类的静态方法,可以被子类继承。

class Foo {
  static classMethod() { // 父类Foo有一个静态方法
    return 'hello';
  }
}

class Bar extends Foo { // 子类Bar可以调用这个方法
}

Bar.classMethod() // 'hello'

静态方法也是可以从super对象上调用的。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
  static classMethod() {
    return super.classMethod() + ', too';
  }
}

Bar.classMethod() // "hello, too"
9.静态属性

静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。

ES6 明确规定,Class 内部只有静态方法,没有静态属性。新写法:在实例属性的前面,加上static关键字。

// 老写法:静态属性定义在类的外部。
class Foo {
  // ...
}
Foo.prop = 1; // 整个类生成以后,再生成静态属性

// 新写法 显式声明(declarative),不是赋值处理,语义更好
class Foo {
  static prop = 1;
}
10.私有方法和私有属性

私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问。

现有写法

这是常见需求,有利于代码的封装,但早期的 ES6 不提供,只能通过变通方法模拟实现。

1)在命名上加以区别。

class Widget {

  // 公有方法
  foo (baz) {
    this._bar(baz);
  }

  // 私有方法
  _bar(baz) {
    return this.snaf = baz;
  }

  // ...
}

上面代码中,_bar()方法前面的下划线,表示这是一个只限于内部使用的私有方法。但是,这种命名是不保险的,在类的外部,还是可以调用到这个方法。

2)将私有方法移出类,因为类内部的所有方法都是对外可见的。

class Widget {
  foo (baz) {
    bar.call(this, baz);
  }

  // ...
}

function bar(baz) {
  return this.snaf = baz;
}

上面代码中,foo是公开方法,内部调用了bar.call(this, baz)。这使得bar()实际上成为了当前类的私有方法。

3)是利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值。

const bar = Symbol('bar');
const snaf = Symbol('snaf');

export default class myClass{

  // 公有方法
  foo(baz) {
    this[bar](baz);
  }

  // 私有方法
  [bar](baz) {
    return this[snaf] = baz;
  }

  // ...
};

上面代码中,barsnaf都是Symbol值,一般情况下无法获取到它们,因此达到了私有方法和私有属性的效果。但是也不是绝对不行,Reflect.ownKeys()依然可以拿到它们。

const inst = new myClass();

Reflect.ownKeys(myClass.prototype)
// [ 'constructor', 'foo', Symbol(bar) ]

上面代码中,Symbol 值的属性名依然可以从类的外部拿到。


私有属性的写法

class加私有属性方法:在属性名之前,使用#表示

class IncreasingCounter {
  #count = 0;
  get value() {
    console.log('Getting the current value!');
    return this.#count;
  }
  increment() {
    this.#count++;
  }
}

上面代码中,#count就是私有属性,只能在类的内部使用(this.#count)。如果在类的外部使用,就会报错。

注意:私有属性的属性名必须包括#,如果不带#,会被当作另一个属性。

class Point {
  #x;

  constructor(x = 0) {
    this.#x = +x;
  }

  get x() {
    return this.#x;
  }

  set x(value) {
    this.#x = +value;
  }
}

上面代码中,#x就是私有属性,在Point类之外是读取不到这个属性的。由于井号#是属性名的一部分,使用时必须带有#一起使用,所以#xx是两个不同的属性。

这种写法不仅可以写私有属性,还可以用来写私有方法。

class Foo {
  #a;
  #b;
  constructor(a, b) {
    this.#a = a;
    this.#b = b;
  }
  #sum() {
    return this.#a + this.#b;
  }
  printSum() {
    console.log(this.#sum());
  }
}

上面代码中,#sum()就是一个私有方法。

私有属性也可以设置 getter 和 setter 方法。

class Counter {
  #xValue = 0;

  constructor() {
    super();
    // ...
  }

  get #x() { return #xValue; } // #x是一个私有属性,它的读写都通过get #x()和set #x()来完成
  set #x(value) {
    this.#xValue = value;
  }
}

私有属性不限于从this引用,只要是在类的内部,实例也可以引用私有属性。

class Foo {
  #privateValue = 42;
  static getPrivateValue(foo) {
    return foo.#privateValue;
  }
}

Foo.getPrivateValue(new Foo()); // 42 允许从实例foo上面引用私有属性

私有属性和私有方法前面,也可以加上static关键字,表示这是一个静态的私有属性或私有方法。


in 运算符

ES2022 改进了in运算符,可以用来判断私有属性。

class C {
  #brand;

  static isC(obj) {
    if (#brand in obj) {
      // 私有属性 #brand 存在
      return true;
    } else {
      // 私有属性 #foo 不存在
      return false;
    }
  }
}

上面示例中,in运算符判断某个对象是否有私有属性#foo。它不会报错,而是返回一个布尔值。

这种用法的in,也可以跟this一起配合使用。

class A {
  #foo = 0;
  m() {
    console.log(#foo in this); // true
    console.log(#bar in this); // false
  }
}

注意,判断私有属性时,in只能用在类的内部。

子类从父类继承的私有属性,也可以使用in运算符来判断。

class A {
  #foo = 0;
  static test(obj) {
    console.log(#foo in obj);
  }
}

class SubA extends A {};

A.test(new SubA()) // true

上面示例中,SubA从父类继承了私有属性#fooin运算符也有效。

注意,in运算符对于Object.create()Object.setPrototypeOf形成的继承,是无效的,因为这种继承不会传递私有属性。

class A {
  #foo = 0;
  static test(obj) {
    console.log(#foo in obj);
  }
}
const a = new A();

const o1 = Object.create(a);
A.test(o1) // false
A.test(o1.__proto__) // true

const o2 = {};
Object.setPrototypeOf(o2, a);
A.test(o2) // false
A.test(o2.__proto__) // true

上面示例中,对于修改原型链形成的继承,子类都取不到父类的私有属性,所以in运算符无效。

11.静态块

静态块(static block):允许在类的内部设置一个代码块,在类生成时运行一次,主要作用是对静态属性进行初始化。

class C {
  static x = ...;
  static y;
  static z;

  static { // 静态块,将静态属性y和z的初始化逻辑,写入了类的内部,而且只运行一次。
    try {
      const obj = doSomethingWith(this.x);
      this.y = obj.y;
      this.z = obj.z;
    }
    catch {
      this.y = ...;
      this.z = ...;
    }
  }
}

每个类只能有一个静态块,在静态属性声明后运行。静态块的内部不能有return语句。

静态块内部可以使用类名或this,指代当前类。

class C {
  static x = 1;
  static {
    this.x; // 1
    // 或者
    C.x; // 1
  }
}

除了静态属性的初始化,静态块还有一个作用,将私有属性与类的外部代码分享。

let getX;

export class C {
  #x = 1;
  static {
    getX = obj => obj.#x;
  }
}

console.log(getX(new C())); // 1

上面示例中,#x是类的私有属性,如果类外部的getX()方法希望获取这个属性,以前是要写在类的constructor()方法里面,这样的话,每次新建实例都会定义一次getX()方法。现在可以写在静态块里面,这样的话,只在类生成时定义一次。

12.类的注意点
严格模式

类和模块的内部,默认就是严格模式。

不存在提升

类不存在变量提升(hoist),这一点与 ES5 完全不同。

new Foo(); // ReferenceError
class Foo {}

上面代码中,Foo类使用在前,定义在后,这样会报错,因为 ES6 不会把类的声明提升到代码头部。必须保证子类在父类之后定义。

{
  let Foo = class {};
  class Bar extends Foo {
  }
}

上面的代码不会报错,因为Bar继承Foo的时候,Foo已经有定义了。但是,如果存在class的提升,上面代码就会报错,因为class会被提升到代码头部,而let命令是不提升的,所以导致Bar继承Foo的时候,Foo还没有定义。

name 属性

由于本质上,ES6 的类只是 ES5 的构造函数的一层包装,所以函数的许多特性都被Class继承,包括name属性。

class Point {}
Point.name // "Point"

name属性总是返回紧跟在class关键字后面的类名。

Generator 方法

如果某个方法之前加上星号(*),就表示该方法是一个 Generator 函数。

class Foo {
  constructor(...args) {
    this.args = args;
  }
  * [Symbol.iterator]() {
    for (let arg of this.args) {
      yield arg;
    }
  }
}

for (let x of new Foo('hello', 'world')) {
  console.log(x);
}
// hello
// world

上面代码中,Foo类的Symbol.iterator方法前有一个星号,表示该方法是一个 Generator 函数。Symbol.iterator方法返回一个Foo类的默认遍历器,for...of循环会自动调用这个遍历器。

this 的指向

类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。

class Logger {
  printName(name = 'there') {
    this.print(`Hello ${name}`);
  }

  print(text) {
    console.log(text);
  }
}

const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined

上面代码中,printName方法中的this,默认指向Logger类的实例。但是,如果将这个方法提取出来单独使用,this会指向该方法运行时所在的环境(由于 class 内部是严格模式,所以 this 实际指向的是undefined),从而导致找不到print方法而报错。

解决方法一:在构造方法中绑定this,这样就不会找不到print方法了。

class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
  }

  // ...
}

解决方法二:使用箭头函数。

class Obj {
  constructor() {
    this.getThis = () => this;
  }
}

const myObj = new Obj();
myObj.getThis() === myObj // true

箭头函数内部的this总是指向定义时所在的对象。上面代码中,箭头函数位于构造函数内部,它的定义生效的时候,是在构造函数执行的时候。这时,箭头函数所在的运行环境,肯定是实例对象,所以this会总是指向实例对象。

解决方法三:使用Proxy,获取方法的时候,自动绑定this

function selfish (target) {
  const cache = new WeakMap();
  const handler = {
    get (target, key) {
      const value = Reflect.get(target, key);
      if (typeof value !== 'function') {
        return value;
      }
      if (!cache.has(value)) {
        cache.set(value, value.bind(target));
      }
      return cache.get(value);
    }
  };
  const proxy = new Proxy(target, handler);
  return proxy;
}

const logger = selfish(new Logger());
13.new.target 属性

ES6 为new命令引入了一个new.target属性,用在构造函数之中,返回new命令作用于的那个构造函数。

如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined

这个属性可以用来确定构造函数是怎么调用的。

// 确保构造函数只能通过new命令调用的代码
function Person(name) {
  if (new.target !== undefined) {
    this.name = name;
  } else {
    throw new Error('必须使用 new 命令生成实例');
  }
}

// 另一种写法
function Person(name) {
  if (new.target === Person) {
    this.name = name;
  } else {
    throw new Error('必须使用 new 命令生成实例');
  }
}

var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三');  // 报错

Class 内部调用new.target,返回当前 Class。

class Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
    this.length = length;
    this.width = width;
  }
}

var obj = new Rectangle(3, 4); // 输出 true

需要注意的是,子类继承父类时,new.target会返回子类。

class Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
    // ...
  }
}

class Square extends Rectangle {
  constructor(length, width) {
    super(length, width);
  }
}

var obj = new Square(3); // 输出 false

上面代码中,new.target会返回子类。

利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。

class Shape {
  constructor() {
    if (new.target === Shape) {
      throw new Error('本类不能实例化');
    }
  }
}

class Rectangle extends Shape {
  constructor(length, width) {
    super();
    // ...
  }
}

var x = new Shape();  // 报错
var y = new Rectangle(3, 4);  // 正确

上面代码中,Shape类不能被实例化,只能用于继承。

注意,在函数外部,使用new.target会报错。

Class 的继承

1.简介

Class 可以通过extends关键字实现继承,让子类继承父类的属性和方法。

class Point { // 父类Point
}

class ColorPoint extends Point { // 子类ColorPoint通过extends关键字,继承了Point类的所有属性和方法
}

子类必须在constructor()方法中调用super()

原因:ES6 的继承机制,则是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”。所以ES6 的继承必须先调用super()方法,生成一个继承父类的this对象,没有这一步就无法继承父类。

class Point { /* ... */ }

class ColorPoint extends Point {
  constructor(x, y, color) { // super在这里表示父类的构造函数,用来新建一个父类的实例对象。
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

注意

  • 新建子类实例时,父类的构造函数必定会先运行一次。

  • 在子类的构造函数中,只有调用super()之后,才可以使用this关键字,否则会报错。


如果子类没有定义constructor()方法,这个方法会默认添加,并且里面会调用super()

class ColorPoint extends Point {
}

// 等同于
class ColorPoint extends Point {
  constructor(...args) {
    super(...args);
  }
}

有了子类的定义,就可以生成子类的实例了。

let cp = new ColorPoint(25, 8, 'green');

cp instanceof ColorPoint // true
cp instanceof Point // true 实例对象cp同时是ColorPoint和Point两个类的实例

除了私有属性,父类的所有属性和方法,都会被子类继承,其中包括静态方法。

class A {
  static hello() {
    console.log('hello world');
  }
}

class B extends A {
}

B.hello()  // hello world

上面代码中,hello()A类的静态方法,B继承A,也继承了A的静态方法。

子类无法继承父类的私有属性,或者说,私有属性只能在定义它的 class 里面使用。

class Foo {
  #p = 1;
  #m() {
    console.log('hello');
  }
}

class Bar extends Foo {
  constructor() {
    super();
    console.log(this.#p); // 报错
    this.#m(); // 报错
  }
}

上面示例中,子类 Bar 调用父类 Foo 的私有属性或私有方法,都会报错。

如果父类定义了私有属性的读写方法,子类就可以通过这些方法,读写私有属性。

class Foo {
  #p = 1;
  getP() {
    return this.#p;
  }
}

class Bar extends Foo {
  constructor() {
    super();
    console.log(this.getP()); // 1
  }
}

上面示例中,getP()是父类用来读取私有属性的方法,通过该方法,子类就可以读到父类的私有属性。

2.Object.getPrototypeOf()

Object.getPrototypeOf()方法可以用来从子类上获取父类。

class Point { /*...*/ }

class ColorPoint extends Point { /*...*/ }

Object.getPrototypeOf(ColorPoint) === Point
// true

因此,可以使用这个方法判断,一个类是否继承了另一个类。

3.super 关键字

super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。

1)super作为函数调用时,代表父类的构造函数。ES6要求,子类的构造函数必须执行一次super函数。

class A {}

class B extends A {
  constructor() {
    super(); // 代表调用父类的构造函数
  }
}

注意super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B的实例。

super()在这里相当于A.prototype.constructor.call(this)

class A {
  constructor() {
    console.log(new.target.name); // new.target指向当前正在执行的函数
  }
}
class B extends A {
  constructor() {
    super();
  }
}
new A() // A
new B() // B 在`super()`执行时,它指向的是子类`B`的构造函数

作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。

class A {}

class B extends A {
  m() {
    super(); // 报错
  }
}

2)super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

class A {
  p() {
    return 2;
  }
}

class B extends A {
  constructor() {
    super();
    console.log(super.p()); // 2
  }
}

let b = new B();

上面代码中,子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()

这里需要注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。

class A {
  constructor() {
    this.p = 2;
  }
}

class B extends A {
  get m() {
    return super.p;
  }
}

let b = new B();
b.m // undefined

上面代码中,p是父类A实例的属性,super.p就引用不到它。

如果属性定义在父类的原型对象上,super就可以取到。

class A {}
A.prototype.x = 2;

class B extends A {
  constructor() {
    super();
    console.log(super.x) // 2
  }
}

let b = new B();

上面代码中,属性x是定义在A.prototype上面的,所以super.x可以取到它的值。

ES6 规定,在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。

class A {
  constructor() {
    this.x = 1;
  }
  print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  m() {
    super.print();
  }
}

let b = new B();
b.m() // 2

上面代码中,super.print()虽然调用的是A.prototype.print(),但是A.prototype.print()内部的this指向子类B的实例,导致输出的是2,而不是1。也就是说,实际上执行的是super.print.call(this)

由于this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。

class A {
  constructor() {
    this.x = 1;
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
    super.x = 3;
    console.log(super.x); // undefined
    console.log(this.x); // 3
  }
}

let b = new B();

上面代码中,super.x赋值为3,这时等同于对this.x赋值为3。而当读取super.x的时候,读的是A.prototype.x,所以返回undefined

如果super作为对象,用在静态方法之中,这时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

var child = new Child();
child.myMethod(2); // instance 2

上面代码中,super在静态方法之中指向父类,在普通方法之中指向父类的原型对象。

另外,在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。

class A {
  constructor() {
    this.x = 1;
  }
  static print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  static m() {
    super.print();
  }
}

B.x = 3;
B.m() // 3

上面代码中,静态方法B.m里面,super.print指向父类的静态方法。这个方法里面的this指向的是B,而不是B的实例。

注意,使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。

class A {}

class B extends A {
  constructor() {
    super();
    console.log(super); // 报错
  }
}

上面代码中,console.log(super)当中的super,无法看出是作为函数使用,还是作为对象使用,所以 JavaScript 引擎解析代码的时候就会报错。这时,如果能清晰地表明super的数据类型,就不会报错。

class A {}

class B extends A {
  constructor() {
    super();
    console.log(super.valueOf() instanceof B); // true
  }
}

let b = new B();

上面代码中,super.valueOf()表明super是一个对象,因此就不会报错。同时,由于super使得this指向B的实例,所以super.valueOf()返回的是一个B的实例。

最后,由于对象总是继承其他对象的,所以可以在任意一个对象中,使用super关键字。

var obj = {
  toString() {
    return "MyObject: " + super.toString();
  }
};

obj.toString(); // MyObject: [object Object]
4.类的 prototype 属性和__proto__属性

大多数浏览器的 ES5 实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。

Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。

(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。

(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

class A {
}

class B extends A {
}

B.__proto__ === A // true 子类B的__proto__属性指向父类A,
B.prototype.__proto__ === A.prototype // true 子类B的prototype属性的__proto__属性指向父类A的prototype属性

这样的结果是因为,类的继承是按照下面的模式实现的。

class A {
}

class B {
}

// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);

// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);

const b = new B();

Object.setPrototypeOf方法的实现。

Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}
Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;

Object.setPrototypeOf(B, A);
// 等同于
B.__proto__ = A;

这两条继承链,可以这样理解:作为一个对象,子类(B)的原型(__proto__属性)是父类(A);

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

B.prototype = Object.create(A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;

extends关键字后面可以跟多种类型的值。

class B extends A {
}

上面代码的A,只要是一个有prototype属性的函数,就能被B继承。

由于函数都有prototype属性(除了Function.prototype函数),因此A可以是任意函数。

下面,讨论两种情况。

第一种,子类继承Object类。

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属性。

5.实例的 proto 属性

子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。也就是说,子类的原型的原型,是父类的原型。

var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');

p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true

上面代码中,ColorPoint继承了Point,导致前者原型的原型是后者的原型。

通过子类实例的__proto__.__proto__属性,可以修改父类实例的行为。

p2.__proto__.__proto__.printName = function () {
  console.log('Ha');
};

p1.printName() // "Ha"

上面代码在ColorPoint的实例p2上向Point类添加方法,结果影响到了Point的实例p1

6.原生构造函数的继承

原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript 的原生构造函数大致有下面这些。

  • Boolean()
  • Number()
  • String()
  • Array()
  • Date()
  • Function()
  • RegExp()
  • Error()
  • Object()

以前,这些原生构造函数是无法继承的,比如,不能自己定义一个Array的子类。

function MyArray() {
  Array.apply(this, arguments);
}

MyArray.prototype = Object.create(Array.prototype, {
  constructor: {
    value: MyArray,
    writable: true,
    configurable: true,
    enumerable: true
  }
});

上面代码定义了一个继承 Array 的MyArray类。但是,这个类的行为与Array完全不一致。

var colors = new MyArray();
colors[0] = "red";
colors.length  // 0

colors.length = 0;
colors[0]  // "red"

之所以会发生这种情况,是因为子类无法获得原生构造函数的内部属性,通过Array.apply()或者分配给原型对象都不行。原生构造函数会忽略apply方法传入的this,也就是说,原生构造函数的this无法绑定,导致拿不到内部属性。

ES5 是先新建子类的实例对象this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。

下面的例子中,我们想让一个普通对象继承Error对象。

var e = {};

Object.getOwnPropertyNames(Error.call(e))
// [ 'stack' ]

Object.getOwnPropertyNames(e)
// []

上面代码中,我们想通过Error.call(e)这种写法,让普通对象e具有Error对象的实例属性。但是,Error.call()完全忽略传入的第一个参数,而是返回一个新对象,e本身没有任何变化。这证明了Error.call(e)这种写法,无法继承原生构造函数。

ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。下面是一个继承Array的例子。

class MyArray extends Array {
  constructor(...args) {
    super(...args);
  }
}

var arr = new MyArray();
arr[0] = 12;
arr.length // 1

arr.length = 0;
arr[0] // undefined

上面代码定义了一个MyArray类,继承了Array构造函数,因此就可以从MyArray生成数组的实例。这意味着,ES6 可以自定义原生数据结构(比如ArrayString等)的子类,这是 ES5 无法做到的。

上面这个例子也说明,extends关键字不仅可以用来继承类,还可以用来继承原生的构造函数。因此可以在原生数据结构的基础上,定义自己的数据结构。下面就是定义了一个带版本功能的数组。

class VersionedArray extends Array {
  constructor() {
    super();
    this.history = [[]];
  }
  commit() {
    this.history.push(this.slice());
  }
  revert() {
    this.splice(0, this.length, ...this.history[this.history.length - 1]);
  }
}

var x = new VersionedArray();

x.push(1);
x.push(2);
x // [1, 2]
x.history // [[]]

x.commit();
x.history // [[], [1, 2]]

x.push(3);
x // [1, 2, 3]
x.history // [[], [1, 2]]

x.revert();
x // [1, 2]

上面代码中,VersionedArray会通过commit方法,将自己的当前状态生成一个版本快照,存入history属性。revert方法用来将数组重置为最新一次保存的版本。除此之外,VersionedArray依然是一个普通数组,所有原生的数组方法都可以在它上面调用。

下面是一个自定义Error子类的例子,可以用来定制报错时的行为。

class ExtendableError extends Error {
  constructor(message) {
    super();
    this.message = message;
    this.stack = (new Error()).stack;
    this.name = this.constructor.name;
  }
}

class MyError extends ExtendableError {
  constructor(m) {
    super(m);
  }
}

var myerror = new MyError('ll');
myerror.message // "ll"
myerror instanceof Error // true
myerror.name // "MyError"
myerror.stack
// Error
//     at MyError.ExtendableError
//     ...

注意,继承Object的子类,有一个行为差异

class NewObj extends Object{
  constructor(){
    super(...arguments);
  }
}
var o = new NewObj({attr: true});
o.attr === true  // false

上面代码中,NewObj继承了Object,但是无法通过super方法向父类Object传参。这是因为 ES6 改变了Object构造函数的行为,一旦发现Object方法不是通过new Object()这种形式调用,ES6 规定Object构造函数会忽略参数。

7.Mixin 模式的实现

Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。

它的最简单实现如下。

const a = {
  a: 'a'
};
const b = {
  b: 'b'
};
const c = {...a, ...b}; // {a: 'a', b: 'b'}

上面代码中,c对象是a对象和b对象的合成,具有两者的接口。

下面是一个更完备的实现,将多个类的接口“混入”(mix in)另一个类。

function mix(...mixins) {
  class Mix {
    constructor() {
      for (let mixin of mixins) {
        copyProperties(this, new mixin()); // 拷贝实例属性
      }
    }
  }

  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) {
  // ...
}

Module 的语法

1.概述

模块(module):将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。

在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

// 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的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。

ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

// ES6模块
import { stat, exists, readFile } from 'fs';

上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。

由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。

除了静态加载带来的各种好处,ES6 模块还有以下好处。

  • 不再需要UMD模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。
  • 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者navigator对象的属性。
  • 不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。
2.严格模式

ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";

严格模式主要有以下限制。

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

上面这些限制,模块都必须遵守。

其中,尤其需要注意this的限制。ES6 模块之中,顶层的this指向undefined,即不应该在顶层代码使用this

3.export 命令

模块功能主要由两个命令构成:exportimport

  • export命令用于规定模块的对外接口

  • import命令用于输入其他模块提供的功能。

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个 JS 文件,里面使用export命令输出变量。

// profile.js     ES6将其视为一个模块,里面用export命令对外部输出了三个变量。
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958; // 用于保存用户信息

export的写法,除了像上面这样,还有另外一种。

// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export { firstName, lastName, year };

上面代码在export命令后面,使用大括号指定所要输出的一组变量。优先使用

export命令除了输出变量,还可以输出函数或类(class)。

export function multiply(x, y) { // 对外输出一个函数multiply
  return x * y;
};

通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。

function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1, // 重命名了函数v1和v2的对外接口
  v2 as streamV2,
  v2 as streamLatestVersion // 重命名后,v2可以用不同的名字输出两次。
};

需要特别注意的是export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

// 报错
export 1;

// 报错
var m = 1;
export m;

上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出 1,第二种写法通过变量m,还是直接输出 1。1只是一个值,不是接口。正确的写法是下面这样。

// 写法一
export var m = 1;

// 写法二
var m = 1;
export {m};

// 写法三
var n = 1;
export {n as m};

上面三种写法都是正确的,规定了对外的接口m。其他脚本可以通过这个接口,取到值1。它们的实质是,在接口名与模块内部变量之间,建立了一一对应的关系。

同样的,functionclass的输出,也必须遵守这样的写法。

// 报错
function f() {}
export f;

// 正确
export function f() {};

// 正确
function f() {}
export {f};

另外,export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。

export var foo = 'bar';
setTimeout(() => foo = 'baz', 500); // 输出变量`foo`,值为`bar`,500 毫秒之后变成`baz`

CommonJS 模块输出的是值的缓存,不存在动态更新。

export命令可以出现在模块的任何位置,只要处于模块顶层就可以。

function foo() { // 如果处于块级作用域内,就会报错--没法做静态优化,违背了 ES6 模块的设计初衷
  export default 'bar' // SyntaxError
}
foo()
4.import 命令

使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。

// main.js
import { firstName, lastName, year } from './profile.js';

function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}

上面代码的import命令,用于加载profile.js文件,并从中输入变量。import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。

如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名

import { lastName as surname } from './profile.js'; 

import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。

import {a} from './xxx.js' // 脚本加载了变量a

a = {}; // Syntax Error : 'a' is read-only;对其重新赋值就会报错
import {a} from './xxx.js'

a.foo = 'hello'; // 合法操作   如果`a`是一个对象,改写`a`的属性是允许的

上面代码中,a的属性可以成功改写,并且其他模块也可以读到改写后的值。不过,这种写法很难查错,建议凡是输入的变量,都当作完全只读,不要轻易改变它的属性。

import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径。

如果不带有路径,只是一个模块名,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。

import { myMethod } from 'util'; //`util`是模块文件名,不带有路径,必须通过配置告诉引擎怎么取到这个模块。

注意import命令具有提升效果,会提升到整个模块的头部,首先执行。本质:import命令是编译阶段执行的,在代码运行之前。

foo();

import { foo } from 'my_module';

import是静态执行,不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

// 报错  表达式
import { 'f' + 'oo' } from 'my_module'; 

// 报错  变量
let module = 'my_module';
import { foo } from module;

// 报错  `if`结构
if (x === 1) {
  import { foo } from 'module1';
} else {
  import { foo } from 'module2';
}

最后,import语句会执行所加载的模块,因此可以有下面的写法。

import 'lodash';

上面代码仅仅执行lodash模块,但是不输入任何值。

如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。

import 'lodash';
import 'lodash'; // 加载了两次`lodash`,但是只会执行一次
import { foo } from 'my_module'; // `foo`和`bar`在两个语句中加载,对应的是同一个`my_module`模块
import { bar } from 'my_module';

// 等同于
import { foo, bar } from 'my_module';

目前阶段,通过 Babel 转码,CommonJS 模块的require命令和 ES6 模块的import命令,可以写在同一个模块里面,但是最好不要这样做。因为import在静态解析阶段执行,所以它是一个模块之中最早执行的。下面的代码可能不会得到预期结果。

require('core-js/modules/es6.symbol');
require('core-js/modules/es6.promise');
import React from 'React';
5.模块的整体加载

用星号(*)指定一个对象,所有输出值都加载在这个对象上面。

下面是一个circle.js文件,它输出两个方法areacircumference

// circle.js

export function area(radius) {
  return Math.PI * radius * radius;
}

export function circumference(radius) {
  return 2 * Math.PI * radius;
}

现在,加载这个模块。

// main.js

import { area, circumference } from './circle';

console.log('圆面积:' + area(4));
console.log('圆周长:' + circumference(14));

上面写法是逐一指定要加载的方法,整体加载的写法如下。

import * as circle from './circle';

console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));

注意,模块整体加载所在的那个对象(上例是circle),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的。

import * as circle from './circle';

// 下面两行都是不允许的
circle.foo = 'hello';
circle.area = function () {};
6.export default 命令

从前面的例子可以看出,使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。

为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

// export-default.js
export default function () {
  console.log('foo');
}

上面代码是一个模块文件export-default.js,它的默认输出是一个函数。

其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

// import-default.js
import customName from './export-default';
customName(); // 'foo'

上面代码的import命令,可以用任意名称指向export-default.js输出的方法,这时就不需要知道原模块输出的函数名。

需要注意的是,import命令后面,不使用大括号。

export default命令用在非匿名函数前,也是可以的。

// export-default.js
export default function foo() { // `foo`函数的函数名`foo`,在模块外部是无效的。加载的时候,视同匿名函数加载
  console.log('foo');
}

// 或者写成

function foo() {
  console.log('foo');
}

export default foo;

下面比较一下默认输出和正常输出。

// 第一组
export default function crc32() { // 输出
  // ...
}

import crc32 from 'crc32'; // 输入

// 第二组
export function crc32() { // 输出
  // ...
};

import {crc32} from 'crc32'; // 输入

第一组是使用export default时,对应的import语句不需要使用大括号(原因:export default命令用于指定模块的默认输出。一个模块只能有一个默认输出,export default命令只能使用一次。)

第二组是不使用export default时,对应的import语句需要使用大括号。

本质上export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。

// modules.js
function add(x, y) {
  return x * y;
}
export {add as default};
// 等同于
// export default add;

// app.js
import { default as foo } from 'modules';
// 等同于
// import foo from 'modules';

正是因为export default命令其实只是输出一个叫做default的变量,所以它后面不能跟变量声明语句。

// 正确
export var a = 1;

// 正确
var a = 1;
export default a; // 含义:将变量`a`的值赋给变量`default`

// 错误
export default var a = 1;

同样地,因为export default命令的本质是将后面的值,赋给default变量,所以可以直接将一个值写在export default之后。

// 正确
export default 42; // 指定对外接口为`default`

// 报错
export 42; // 没有指定对外的接口

有了export default命令,输入模块时就非常直观了,以输入 lodash 模块为例。

import _ from 'lodash';

如果想在一条import语句中,同时输入默认方法和其他接口,可以写成下面这样。

import _, { each, forEach } from 'lodash';

对应上面代码的export语句如下。

export default function (obj) {
  // ···
}

export function each(obj, iterator, context) {
  // ···
}

export { each as forEach };

上面代码的最后一行的意思是,暴露出forEach接口,默认指向each接口,即forEacheach指向同一个方法。

export default也可以用来输出类。

// MyClass.js
export default class { ... }

// main.js
import MyClass from 'MyClass';
let o = new MyClass();
7.export 与 import 的复合写法

如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。

export { foo, bar } from 'my_module';

// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };

上面代码中,exportimport语句可以结合在一起,写成一行。但需要注意的是,写成一行以后,foobar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foobar

模块的接口改名和整体输出,也可以采用这种写法。

// 接口改名
export { foo as myFoo } from 'my_module';

// 整体输出
export * from 'my_module';

默认接口的写法如下。

export { default } from 'foo';

具名接口改为默认接口的写法如下。

export { es6 as default } from './someModule';

// 等同于
import { es6 } from './someModule';
export default es6;

同样地,默认接口也可以改名为具名接口。

export { default as es6 } from './someModule';

ES2020 之前,有一种import语句,没有对应的复合写法。

import * as someIdentifier from "someModule";

ES2020补上了这个写法。

export * as ns from "mod";

// 等同于
import * as ns from "mod";
export {ns};
8.模块的继承

模块之间也可以继承。

假设有一个circleplus模块,继承了circle模块。

// circleplus.js

export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
  return Math.exp(x);
}

上面代码中的export *,表示再输出circle模块的所有属性和方法。

注意,export *命令会忽略circle模块的default方法。然后,上面代码又输出了自定义的e变量和默认方法。

这时,也可以将circle的属性或方法,改名后再输出。

// circleplus.js

export { area as circleArea } from 'circle'; // 只输出`circle`模块的`area`方法,且将其改名为`circleArea`。

加载上面模块的写法如下。

// main.js

import * as math from 'circleplus';
import exp from 'circleplus';
console.log(exp(math.e));

上面代码中的import exp表示,将circleplus模块的默认方法加载为exp方法。

9.跨模块常量

const声明的常量只在当前代码块有效。如果想设置跨模块的常量(即跨多个文件),或者说一个值要被多个模块共享,可以采用下面的写法。

// constants.js 模块
export const A = 1;
export const B = 3;
export const C = 4;

// test1.js 模块
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3

// test2.js 模块
import {A, B} from './constants';
console.log(A); // 1
console.log(B); // 3

如果要使用的常量非常多,可以建一个专门的constants目录,将各种常量写在不同的文件里面,保存在该目录下。

// constants/db.js
export const db = {
  url: 'http://my.couchdbserver.local:5984',
  admin_username: 'admin',
  admin_password: 'admin password'
};

// constants/user.js
export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];

然后,将这些文件输出的常量,合并在index.js里面。

// constants/index.js
export {db} from './db';
export {users} from './users';

使用的时候,直接加载index.js就可以了。

// script.js
import {db, users} from './constants/index';
10.import()
1)简介

import命令会被 JavaScript 引擎静态分析,先于模块内的其他语句执行(import命令叫做“连接” binding 其实更合适)。所以,下面的代码会报错。

// 报错
if (x === 2) {
  import MyModual from './myModual';
}

上面代码中,引擎处理import语句是在编译时,这时不会去分析或执行if语句,所以import语句放在if代码块之中毫无意义,因此会报句法错误,而不是执行时错误。

importexport命令只能在模块的顶层,不能在代码块之中(比如,在if代码块之中,或在函数之中)。

这样的设计,固然有利于编译器提高效率,但也导致无法在运行时加载模块。在语法上,条件加载就不可能实现。如果import命令要取代 Node 的require方法,这就形成了一个障碍。因为require是运行时加载模块,import命令无法取代require的动态加载功能。

const path = './' + fileName;
const myModual = require(path);

上面的语句就是动态加载,require到底加载哪一个模块,只有运行时才知道。import命令做不到这一点。

ES2020提案 引入import()函数,支持动态加载模块。

import(specifier)

上面代码中,import函数的参数specifier,指定所要加载的模块的位置。

import命令能够接受什么参数,import()函数就能接受什么参数,两者区别主要是后者为动态加载。

import()返回一个 Promise 对象。下面是一个例子。

const main = document.querySelector('main');

import(`./section-modules/${someVariable}.js`)
  .then(module => {
    module.loadPageInto(main);
  })
  .catch(err => {
    main.textContent = err.message;
  });

import()函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。

import()函数与所加载的模块没有静态连接关系,这点也是与import语句不相同。

import()类似于 Node.js 的require()方法,区别主要是前者是异步加载,后者是同步加载。

由于import()返回 Promise 对象,所以需要使用then()方法指定处理函数。考虑到代码的清晰,更推荐使用await命令。

async function renderWidget() {
  const container = document.getElementById('widget');
  if (container !== null) {
    // 等同于
    // import("./widget").then(widget => {
    //   widget.render(container);
    // });
    const widget = await import('./widget.js');
    widget.render(container);
  }
}

renderWidget();

上面示例中,await命令后面就是使用import(),对比then()的写法明显更简洁易读。

2)适用场合

下面是import()的一些适用场合。

(1)按需加载。

import()可以在需要的时候,再加载某个模块。

button.addEventListener('click', event => {
  import('./dialogBox.js')
  .then(dialogBox => {
    dialogBox.open();
  })
  .catch(error => {
    /* Error handling */
  })
});

上面代码中,import()方法放在click事件的监听函数之中,只有用户点击了按钮,才会加载这个模块。

(2)条件加载

import()可以放在if代码块,根据不同的情况,加载不同的模块。

if (condition) {
  import('moduleA').then(...);
} else {
  import('moduleB').then(...);
}

上面代码中,如果满足条件,就加载模块 A,否则加载模块 B。

(3)动态的模块路径

import()允许模块路径动态生成。

import(f())
.then(...);

上面代码中,根据函数f的返回结果,加载不同的模块。

3)注意点

import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数。

因此,可以使用对象解构赋值的语法,获取输出接口。

import('./myModule.js')
.then(({export1, export2}) => {
  // ...·
});

上面代码中,export1export2都是myModule.js的输出接口,可以解构获得。

如果模块有default输出接口,可以用参数直接获得。

import('./myModule.js')
.then(myModule => {
  console.log(myModule.default);
});

上面的代码也可以使用具名输入的形式。

import('./myModule.js')
.then(({default: theDefault}) => {
  console.log(theDefault);
});

如果想同时加载多个模块,可以采用下面的写法。

Promise.all([
  import('./module1.js'),
  import('./module2.js'),
  import('./module3.js'),
])
.then(([module1, module2, module3]) => {
   ···
});

import()也可以用在 async 函数之中。

async function main() {
  const myModule = await import('./myModule.js');
  const {export1, export2} = await import('./myModule.js');
  const [module1, module2, module3] =
    await Promise.all([
      import('./module1.js'),
      import('./module2.js'),
      import('./module3.js'),
    ]);
}
main();
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值