前端基础知识学习——Javascript&ES6
数据类型
1 基本数据类型(6种)
1) String
a) 用法:
用于存储和处理文本,使用**’ '或" "**。
相同的引号嵌套时,反斜杠转义字符把特殊字符转换为字符串字符,可以使用转义字符转义的特殊字符有:
代码 | 输出 |
---|---|
\’ | 单引号 |
\" | 双引号 |
\\ | 反斜杠 |
\n | 换行 |
\r | 回车 |
\t | tab(制表符) |
\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
,其值不是undefined
或null
的任何对象「所有的对象类型」(包括其值为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]+$
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 |
null | 0 | “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中新增的保留关键字
class | enum | export | extends | import | super |
---|
JavaScript 对象、属性和方法
Array | Date | eval | function | hasOwnProperty |
---|---|---|---|---|
Infinity | isFinite | isNaN | isPrototypeOf | length |
Math | NaN | name | Number | Object |
prototype | String | toString | undefined | valueOf |
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
由于undefined
和null
无法转为对象,所以对它们进行解构赋值,都会报错。
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)。
- 不能两个或两个以上的分隔符连在一起。
- 小数点的前后不能有分隔符。
- 科学计数法里面,表示指数的
e
或E
前后不能有分隔符。
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 - 1Math.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 函数。
-
除了this
,arguments
、super
、new.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()
的返回值是一个对象,该对象的键名就是每一组的组名,即分组函数返回的每一个字符串(上例是even
和odd
);该对象的键值是一个数组,包括所有产生当前键名的原数组成员。
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
属性不是在该方法上面,而是该方法的属性的描述对象的get
和set
属性上面,返回值是方法名前加上get
和set
。
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 // }
有四个操作会忽略
enumerable
为false
的属性。for...in
循环:只遍历对象自身的和继承的可枚举的属性。Object.keys()
:返回对象自身的所有可枚举的属性的键名。JSON.stringify()
:只串行化对象自身的可枚举的属性。Object.assign()
: 忽略enumerable
为false
的属性,只拷贝对象自身的可枚举的属性。(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),将它们连同值一起拷贝过来
如果等号右边是
undefined
或null
,就会报错,因为它们无法转为对象。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时指定默认值,是通过||
运算符指定默认值,但是属性的值如果为空字符串或false
或0
,默认值也会生效。
Null 判断运算符(??
):作用类似||
,但是只有运算符左侧的值为null
或undefined
时,才会返回右侧的值。
const headerText = response.settings.headerText ?? 'Hello, world!';
const animationDuration = response.settings.animationDuration ?? 300;
const showSplashScreen = response.settings.showSplashScreen ?? true;
用法:
-
和链判断运算符
?.
配合使用,为null
或undefined
的值设置默认值。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
构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。
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
注意,调用resolve
或reject
并不会终结 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
的状态由p1
、p2
、p3
决定,分成两种情况。
(1)只有p1
、p2
、p3
的状态都变成fulfilled
,p
的状态才会变成fulfilled
,此时p1
、p2
、p3
的返回值组成一个数组,传递给p
的回调函数。
(2)只要p1
、p2
、p3
之中有一个被rejected
,p
的状态就变成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]);
只要p1
、p2
、p3
之中有一个实例率先改变状态,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)更好的语义
async
和await
,比起星号和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)]
x
和y
都是实例对象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)
在“类”的内部可以使用get
和set
关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
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;
}
// ...
};
上面代码中,bar
和snaf
都是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
类之外是读取不到这个属性的。由于井号#
是属性名的一部分,使用时必须带有#
一起使用,所以#x
和x
是两个不同的属性。
这种写法不仅可以写私有属性,还可以用来写私有方法。
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
从父类继承了私有属性#foo
,in
运算符也有效。
注意,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 可以自定义原生数据结构(比如Array
、String
等)的子类,这是 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
不会在它的外层作用域引入变量eval
和arguments
不能被重新赋值arguments
不会自动反映函数参数的变化- 不能使用
arguments.callee
- 不能使用
arguments.caller
- 禁止
this
指向全局对象 - 不能使用
fn.caller
和fn.arguments
获取函数调用的堆栈 - 增加了保留字(比如
protected
、static
和interface
)
上面这些限制,模块都必须遵守。
其中,尤其需要注意this
的限制。ES6 模块之中,顶层的this
指向undefined
,即不应该在顶层代码使用this
。
3.export 命令
模块功能主要由两个命令构成:export
和import
。
-
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
。它们的实质是,在接口名与模块内部变量之间,建立了一一对应的关系。
同样的,function
和class
的输出,也必须遵守这样的写法。
// 报错
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
文件,它输出两个方法area
和circumference
。
// 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
接口,即forEach
和each
指向同一个方法。
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 };
上面代码中,export
和import
语句可以结合在一起,写成一行。但需要注意的是,写成一行以后,foo
和bar
实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo
和bar
。
模块的接口改名和整体输出,也可以采用这种写法。
// 接口改名
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
代码块之中毫无意义,因此会报句法错误,而不是执行时错误。
import
和export
命令只能在模块的顶层,不能在代码块之中(比如,在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}) => {
// ...·
});
上面代码中,export1
和export2
都是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();