基本用法
let 用于变量声明,类似于var,但其声明的变量只在let命令所在的代码块内有效
{
var a = 10;
let b = 5;
}
console.log(a); // 10
console.log(b); // b is not defined;
for循环的计数器
for (let i = 0; i < 10; i ++) {
// todo
}
console.log(i); // i is not defined;
由此可见,i只在for循环体内有效,循环外引用报错
如下代码,如果使用var ,输出的会是10;
var a = [];
for(var i = 0; i < 10; i ++) {
a[i] = function() {
console.log(i);
}
}
a[6](); // 10; var定义的变量为全局变量,console.log(i)即为全局的i,最后一次循环完后i的值为10;
但如果使用let定义,输出的会是6;
var a = [];
for(let i = 0; i < 10; i ++) {
a[i] = function() {
console.log(i);
}
}
a[6](); // 6 let定义的i,每次循环都只在本轮循环中生效
另外,for循环有个特别之处,设置循环变量的部分是一个父级作用域,而循环体内的部分是一个单独的子作用域
for (let i = 0; i < 3; i ++) {
let i = 'abc';
console.log(i);
}
// 输出3遍 abc, 说明循环变量i和循环体内i在不同的作用域,有各自单独的作用域
不存在变量提升
var 定义变量会存在变量提升
var i = 10;
function fn() {
console.log(i);
var i = 5;
};
fn(); // undefined; 存在变量提升 => var i; console.log(i); i = 5;
let 命令改变了这种语法,let声明的变量一定要在声明后使用,否则报错
let i = 10;
function fn() {
console.log(i);
let i = 5;
};
fn(); // 直接报错
暂时性死区
只要块级作用域存在let命令,它所声明的变量就绑定这个区域,不再受外部影响;
var a = 123;
function fn() {
a = 456;
let a;
}
fn(); // 直接报错,块级作用域中let声明变量之前进行赋值导致报错
总之,在let声明变量之前,该变量都是不可用的,这在语法上,称为“暂时性死区(TDZ)”;
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
不允许重复声明
let不允许在同一作用域内,重复声明变量
function fn() {
let a = 10;
var a = 5; // 报错
}
function fn() {
let a = 10;
let a = 5; // 报错
}
function func(arg) {
let arg;
}
func() // 报错
function func(arg) {
{
let arg;
}
}
func() // 不报错,声明变量不在同一作用域内
为什么需要块级作用域?
ES5只有全局作用域和函数作用域,没有块级作用域,带来许多不合理的场景
第一种场景,内层变量可能会覆盖外层变量
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
ES6的块级作用域
let实际上为Javascript提供了块级作用域
function fn () {
let a = 10;
if (true) {
let a = 5;
}
console.log(i);// 5 外层代码块不受内层代码块影响,如果用var 声明,则会输出10;
}
内层作用域可以定义外层作用域的同名变量。
{{{{
let insane = 'Hello World';
{let insane = 'Hello World'}
}}}};
块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了。
// IIFE 写法
(function () {
var tmp = ...;
...
}());
// 块级作用域写法
{
let tmp = ...;
...
}
基本用法
const 声明一个只读的常量,一旦声明,不能改变
const PI = 3.1415;
console.log(PI); // 3.1415
PI = 4; // 报错
const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。
const foo;
// SyntaxError: Missing initializer in const declaration
const 的作用域与let一样,只在定义变量所在的作用域内有效
if (true) {
const a = 7;
}
console.log(a); // 报错
const 声明的变量同样不存在变量提升,存在暂时性死区,只能声明后使用,不可重复声明;
本质
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const
只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
const foo = {};
foo.prop = 'abc'; // 为其添加属性,成功
foo = {}; // 将foo指向另外一个对象,报错
另一个例子
const a = [];
a.push('Hello'); // 可执行
a.length = 0; // 可执行
a = ['Dave']; // 报错
如果真的想把对象冻结,可以使用Object.freeze()方法
const foo = Object.freeze({});
// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;
除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
顶层对象,在浏览器环境指的是window
对象,在 Node 指的是global
对象。ES5 之中,顶层对象的属性与全局变量是等价的。顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。
window.a = 1;
a // 1
a = 2;
window.a // 2
ES6 为了改变这一点,一方面规定,为了保持兼容性,var
命令和function
命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let
命令、const
命令、class
命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。
var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1
let b = 1;
window.b // undefined
基本用法
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
以前,为变量赋值,只能直接指定值
let a = 1;
let b = 2;
let c = 3;
ES6允许写成下边这样
let [a, b, c] = [1, 2, 3]
本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,等式左边就会被赋予相应的值
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
let [ , , third] = ["foo", "bar", "baz"];
third // "baz"
let [x, , y] = [1, 2, 3];
x // 1
y // 3
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
let [x, y, ...z] = ['a'];
x // "a"
y // undefined 如果解构不成功,值就为undefined
z // []
另一种情况是不完全解构,即等号左边的模式,只匹配一部分等号右边的值数组,也可解构成功
let [x, y] = [1, 2, 3];
x // 1
y // 2
let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4
默认值
解构赋值允许指定默认值
let [foo = true] = [];
foo; // true
let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
注意⚠️:ES6内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined时,默认值才会生效
let [x = 1] = [undefined] // x = 1
let [x = 1] = [null] // null, 因为null 不严格等于 undefined
如果默认值是一个表达式,那么这个表达式是惰性求值的,只有在用到的时候,才会求值
function fn() {
console.log('aaa');
}
let [a = fn()] = [1] // a = 1,因为 a 能取到值,所以fn()不会求值
默认值可以引用解构赋值的其他变量,但该变量必须已经声明
let [x = 1, y = x] = []; // x=1; y=1
let [x = 1, y = x] = [2]; // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = []; // ReferenceError: y is not defined 当时y还没有声明
基本用法
对象的解构与数组有个很重要的不同。数组的元素是按照次序排列的,元素的取值是由他的位置决定;而对象的属性没有次序,变量与属性必须同名,才能取到正确的值
let {foo, bar} = {bar: 'bbb', foo: 'aaa'};
foo; // aaa
bar; // bbb
let {baz} = {bar: 'bbb', foo: 'aaa'};
baz; // undefined
对象的解构赋值,可以很方便的把现有的方法,赋值到某个变量
let {sin, cos, log} = Math;
const {log} = console;
log('hello'); // hello
如果变量名与属性名不一致
let {foo: baz} = {foo: 'aaa', bar: 'bbb'}
baz; // aaa
这实际说明,对象的解构赋值是下面形式的简写
let {foo: foo, bar: bar} = {foo: 'aaa', bar: 'bbb'};
// 也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
下面是赋值嵌套的例子
let obj = {};
let arr = [];
({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });
obj // {prop:123}
arr // [true]
注意,对象的解构赋值可以取到继承的属性
const obj1 = {};
const obj2 = {foo: 'aaa'};
Object.setPrototypeOf(obj1, obj2);
const {foo} = obj1;
foo; // aaa
默认值
默认值生效的条件是,对象的属性值严格等于undefined
。
var {x: y = 3} = {};
y // 3
var {x: y = 3} = {x: 5};
y // 5
var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"
var {x = 3} = {x: undefined};
x // 3
var {x = 3} = {x: null};
x // null
注意点
1、如果要将一个已经声明的变量用于解构赋值,要非常小心
// 错误的写法
let a;
{a} = {a: 'hhh'}; // Javascript会将{a}认为是一个代码块,只有不将大括号写在行首,才会避免错误
// 正确的写法
let a;
({a} = {a: 'hhh'})
2、解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。
({} = [true, false]);
({} = 'abc');
({} = []);
3、由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3
字符串也可以解构赋值。因为此时,字符串被转换成了类似数组的对象
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
类似数组的对象都有一个length
属性,因此还可以对这个属性解构赋值。
let {length : len} = 'hello';
len // 5
解构赋值时,如果等号右边是数值或布尔值,都会先转化为对象
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
//数值和布尔值的包装对象都有toString属性,因此变量s都能取到值
解构赋值的规则是,只要等号右边的值不为对象或数组,都先转换为对象。由于undefined和null不能转换为对象,所以无法解析赋值
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
函数的参数也可以进行解构赋值
function fn([a, b]) {
return a + b;
}
fn([1,2]); // 3
函数参数的解构也可设置默认值
function move({x = 0, y = 0} = {}) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
注意,下面的写法会得到不一样的结果。
// 此代码为函数move的参数指定默认值,而不是为变量x和y指定默认值,所以会得到与前一种写法不同的结果。
function move({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]
undefined
就会触发函数参数的默认值。
[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]
不能使用圆括号的情况
1、变量声明语句
// 全部报错
let [(a)] = [1];
let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};
let { o: ({ p: p }) } = { o: { p: 2 } };
2、函数参数
// 报错
function f([(z)]) { return z; }
// 报错
function f([z,(x)]) { return x; }
3、赋值语句的格式
// 全部报错
({ p: a }) = { p: 42 };
([a]) = [5];
可以使用圆括号的情况
只有一种,赋值语句的非模式部分,可以使用圆括号
[(b)] = [3]; // 正确
({ p: (d) } = {}); // 正确
[(parseInt.prop)] = [3]; // 正确
1、交换变量的值
let x = 1;
let y = 2;
[x, y] = [y, x];
2、从函数返回多个值
// 返回一个数组
function fn () {
return [1, 2, 3]
}
let [x, y, z] = fn()
// 返回一个对象
function fn () {
return {
foo: 'abc',
bar: 'efd'
}
}
let {foo, bar} = fn();
3、函数参数的定义
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
4、提取json数据
let jsonData = {
id: 888,
str: 'hhh',
data: [123,456]
}
let {id, str:name, data:number} = jsonData
console.log(id, name, number) // 888, hhh, [123,456]
5、函数参数的默认值
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config
} = {}) {
// ... do stuff
};
6、遍历map结构
const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');
for (let [key, value] of map) {
console.log(key + " is " + value);
}
// first is hello
// second is world
// 只获取键名
for (let [key] of map) {
console.log(key)
}
// 只获取值
for (let [,value] of map) {
console.log(value)
}
7、输入模块的指定方法
const { SourceMapConsumer, SourceNode } = require("source-map");
模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量
// 普通字符串
let a = `I am a China`;
// 多行字符串
let b = `In JavaScript this is
not legal.`
// 字符串嵌入变量
let name = 'Jason';
let age = 18;
let str = `他的名字叫${name}, 年龄是${age}岁`;
console.log(str); // 他的名字叫Jason, 年龄是18岁
字符串查找
ES5 只有indexOf()方法,用来判断一个字符串是否包含在另一个字符串中
ES6 又提供了3种方法:
includes(): 返回布尔值,表示是否找到了参数字符串;
startWith(): 返回布尔值,表示参数字符串是否在原字符串的开头;
endsWith(): 返回布尔值,表示参数字符串是否在原字符串的结尾;
let str = 'apple banana pear';
// 传统方法 indexOf 返回索引 没找到返回-1
console.log(str.indexOf('banana')) // 6
console.log(str.indexOf('orange')) // -1
// includes 返回true/false
console.log(str.includes('apple')) // true
console.log(str.includes('orange')) // false
// startWith
let str = 'https://study.163.com/course/courseLearn.htm?courseId=1005211046';
let str1 = 'http://www.baidu.com/';
console.log(str.startsWith('https')) // true
console.log(str1.startsWith('https')) // false
// endWith
let imageUrl = 'hhh.png';
let imageUrl1 = 'hhh.jpg';
console.log(imageUrl.endsWith('png')) // true
console.log(imageUrl1.endsWith('png')) // false
这三个方法都支持第二个参数,表示开始搜索的位置。
let s = 'Hello world!';
s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false
使用第二个参数n时,endsWith
与其他两个不同,它针对前n个字符,而其他两个针对从第n个到字符串结束的字符。
重复字符串repeat
该方法返回一个新的字符串,表示将原字符串重复n次
'x'.repeat(3) // "xxx"
参数如果是小数,会被取整
'na'.repeat(2.9) // "nana"
参数是负数或者Infinity
,会报错
'na'.repeat(Infinity)
// RangeError
'na'.repeat(-1)
// RangeError
参数NaN等同于0
'na'.repeat(NaN) // ""
参数是字符串,会先转换成数字
'na'.repeat('3') //nanana
填充字符串 padStart、padEnd
字符串自动补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。
'x'.padStart(5, 'ab'); //ababx
'x'.padEnd(3, 'ab'); // xab
如果原字符串大于或等于最大长度,则字符串补全不生效,返回原字符串
`xxx`.padStart(2, 'ab'); // xxx
如果用来补全的字符串与原字符串,两者的长度之和超过了最大长度,则会截去超出位置的补全字符串
'abc'.padStart(10, '0123456789')
// '0123456abc'
如果省略第二个参数,默认用空格补全长度
'abc'.padStart(6); // 'abc '
padStart
常见用途
// 为数值补全指定位数
'1'.padStart(10, '0') // "0000000001"
// 提示字符串格式
'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
如果不确定填充字符串的长度
let a = 'apple';
let str = 'xxx';
console.log(a.padStart(a.length+str.length, str))
console.log(a.padEnd(a.length+str.length, str))
去除空格方法 trimStart、trimEnd
trimStart
和trimEnd
行为与trim()一致,去除字符串空格。trimStart
消除字符串头部的空格,trimEnd
消除字符串尾部的空格。他们返回的都是新字符串
const s = ' abc ';
s.trim() // "abc"
s.trimStart() // "abc "
s.trimEnd() // " abc"
除了空格键,这两个方法对字符串头部(或尾部)的 tab 键、换行符等不可见的空白符号也有效。
基本用法
ES6之前,不能为函数的参数直接定义默认值,只能采用变通的方法
function log(x,y) {
y = y || 'world'
console.log(x, y)
}
log('hello') // hello
log('hello', 'china') // hello china
log('hello', '') // hello world
ES6允许为函数的参数定义默认值
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
函数的参数默认声明,不能再使用let或const再次声明
function fn (a = 18) {
// let a = 29; // 报错 Identifier 'a' has already been declared,因为函数的参数默认已经定义了。在函数内部不能再使用let或const声明
console.log(a)
}
fn()
使用参数默认值时,函数不能有同名参数
function fn(x,x,y=1) {
console.log(x,y); // 报错 SyntaxError: Duplicate parameter name not allowed in this context
}
另外,一个容易忽略的地方是,参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。
let x = 99;
function foo(p = x + 1) {
console.log(p);
}
foo() // 100
x = 100;
foo() // 101
与解构赋值默认值结合使用
参数默认值可以与解构赋值默认值结合使用
function fn({x = 0, y = 0}) {
console.log(x, y)
}
fn({x: 1, y: 2}); // 1, 2
fn({x: 1}); // 1 0
fn({}); // 0 0
fn(); // TypeError: Cannot read property 'x' of undefined
// 如果没有提供参数,函数foo的参数默认为一个空对象
function fn({x = 0, y = 0} = {}) {
console.log(x, y)
}
fn(); // 0 0
参数默认值的位置
通常情况下,定义参数默认值的位置,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数
function f(x = 1, y) {
return [x, y];
}
f() // [1, undefined]
f(2) // [2, undefined]
f(, 1) // 报错
f(undefined, 1) // [1, 1]
函数的length属性
指定了默认值之后,函数的length属性,将返回没有指定参数默认值的参数个数,即指定默认值后,length属性将失真
(function(a) {}).length // 1
(function(a = 1) {}).length // 0
因为length
属性的含义是,该函数预期传入的参数个数。rest参数也不会记入length
属性
(function(...args) {}).length // 0
如果设置了参数默认值的不是尾参数,那么也不会记入length
属性
(function(a = 1, b, c) {}).length // 0
作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
var x = 1;
function f(x, y = x) {
console.log(y);
}
f(2) // 2 形成一个单独的作用域,x在函数内已经定义,所以不会再指向外部全局的x
let x = 1;
function f(y = x) {
let x = 2;
console.log(y);
}
f() // 1 x本身没有定义,所以指向外部全局的x,不会指向函数内部的局部变量x,如果此时全局没有定义x,则会报错
应用
利用参数默认值,可以指定某个参数不得省略,如果省略就抛出一个错误
function throwIfMissing() {
throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}
foo()
// Error: Missing parameter
另外,可以将参数设置为undefined,说明参数是可以省略的
function fn(optional = undefined){
...
}
ES6 引入了rest参数(…变量名),用于获取函数的多余参数,这样就不需要arguments对象了。rest参数搭配的变量是一个数组,该变量多余的参数放入数组中
function add(...values) {
let sum = 0;
for(var val of values) {
sum += val
}
return sum;
}
add(2,3,5); // 10
rest参数既可以展开数组,也可以重置数组
function fn (...x) {
console.log(x)
}
fn(1,8,0); // [1, 8, 0]
function fn1 (a, b, c) {
console.log(a, b, c)
}
fn1(...[2,3,5]); // 2,3,5
注意,rest参数之后不能有其他参数,即只能是最后一个参数,否则会报错
function fn2 (a, b, ...c) {
console.log(a, b); // 1 2
console.log(c); // [3, 4, 5]
}
fn2(1,2,3,4,5)
ES2016规定,只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显示设置为严格模式,否则会报错
// 报错
function doSomething(a, b = a) {
'use strict';
// code
}
// 报错
const doSomething = function ({a, b}) {
'use strict';
// code
};
// 报错
const doSomething = (...a) => {
'use strict';
// code
};
const obj = {
// 报错
doSomething({a, b}) {
'use strict';
// code
}
};
函数的name属性,返回该函数的函数名。如果将一个匿名函数赋给一个变量,ES5的name
会返回一个空字符串,而ES6会返回实际的函数名
var f = function() {
...
}
// ES5
f.name; // ''
// ES6
f.name; // f
如果将一个具名函数赋给一个变量,ES5和ES6的name
都会返回函数原本的名字
const bar = function baz() {};
// ES5
bar.name // "baz"
// ES6
bar.name // "baz"
基本用法
ES6允许使用箭头(=>)定义函数
let x = (a, b) => (a+b)
console.log(x(5,9)) // 14
箭头函数的一个用处是简化回调函数
// 正常函数写法
[1,2,3].map(function(x) {
return x*x;
})
// 箭头函数写法
[1,2,3].map(x => x*x)
另一个用处是
// 正常函数写法
var result = values.sort(function(x,y) {
return x - y
})
// 箭头函数写法
var result = values.sort((x, y) => (x - y))
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]]
使用注意点
1、函数体内的this
对象,就是定义时所在的对象,而不是调用时所在的对象;
2、不可以当构造函数,也就是说,不可以使用new
命令,否则会抛出一个错误;
3、不可以使用arguments
对象,该对象在函数体内不存在,如果要用,可以使用rest
参数代替;
4、不可以使用yield
命令,因此箭头函数不能用作 Generator 函数。
上面四点中,第一点尤其注意⚠️,this
对象的指向是可变的,但是在箭头函数中,它是固定的
var id = 123;
function fn() {
setTimeout(() => {
console.log(this.id)
}, 1000)
}
fn.call({id: 10}); // 10 this指向函数定义时所在的对象 即{id: 10}
function Timer() {
this.s1 = 0;
this.s2 = 0;
// 箭头函数
setInterval(() => this.s1++, 1000);
// 普通函数
setInterval(function () {
this.s2++;
}, 1000);
}
var timer = new Timer();
setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3 绑定定义时所在的作用域,即Timer函数,所以会执行3次
// s2: 0 指向全局对象
this
指向的固定化,并不是因为箭头函数内部有绑定this
的机制,实际原因是箭头函数根本没有自己的this
,导致内部的this
就是外层代码块的this
。正是因为它没有this
,所以也就不能用作构造函数。
除了this
,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments
、super
、new.target
。
function foo() {
setTimeout(() => {
console.log('args:', arguments); // 此arguments实际上是箭头函数foo的arguments变量
}, 100);
}
foo(2, 4, 6, 8)
// args: [2, 4, 6, 8]
另外,由于箭头函数没有自己的this
,所以当然也就不能用call()
、apply()
、bind()
这些方法去改变this
的指向。
(function() {
return [
(() => this.x).bind({ x: 'inner' })()
];
}).call({ x: 'outer' });
// ['outer']
不适用场合
第一个场合是定义对象的方法,且该方法中包含this
let cat = {
lives: 9,
jumps: ()=> {
this.lives --;
}
}
// NAN 因为对象不构成单独的作用域,导致jumps箭头函数定义时的作用域就是全局作用域
第二个场合是需要动态this
的时候,也不应使用箭头函数
var button = document.getElementById('press');
button.addEventListener('click', () => {
this.classList.toggle('on');
});
// button的监听函数是一个箭头函数,导致里面的this就是全局对象
嵌套的箭头函数
箭头函数内部,还可以再使用箭头函数
什么是尾调用
尾调用就是指一个函数的最后一步是调用另一个函数
function f(x) {
return g(x);
}
以下3种情况中,都不属于尾调用
// 情况1
function fn(x) {
let a = g(x);
return a; // 调用函数之后,还有赋值操作,不属于尾调用
}
// 情况2
function fn(x) {
return g(x) + 1; // 调用后还有操作,不属于尾调用
}
// 情况3
function fn(x) {
g(x); // 等同于 g(x); return undefined;
}
尾调用不一定在函数尾部,只要在最后一步调用即可
function fn(x) {
if (x > 0) {
return a(x)
}
return b(x)
}
// 函数 a 和 b都属于尾调用,都是函数fn 的最后一步操作
尾调用优化
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同于
function f() {
return g(3);
}
f();
// 等同于
g(3);
上面代码中,如果函数g
不是尾调用,函数f
就需要保存内部变量m
和n
的值、g
的调用位置等信息。但由于调用g
之后,函数f
就结束了,所以执行到最后一步,完全可以删除f(x)
的调用帧,只保留g(3)
的调用帧。
这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。
尾递归
函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) // 120
上面代码是一个阶乘函数,计算n
的阶乘,最多需要保存n
个调用记录,复杂度 O(n)
如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
严格模式
ES6的尾调用优化只在严格模式下开启,正常模式是无效的
这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。
func.arguments
:返回调用时函数的参数。func.caller
:返回调用当前函数的那个函数。尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。
function restricted() {
'use strict';
restricted.caller; // 报错
restricted.arguments; // 报错
}
restricted();
ES2017允许函数的最后一个参数有尾逗号
function clownsEverywhere(
param1,
param2,
) { /* ... */ }
clownsEverywhere(
'foo',
'bar',
);
toString()
方法返回函数代码本身,以前会省略注释和空格
function /* foo comment */ foo () {}
foo.toString()
// function foo() {}
修改后的toString()
方法,明确要求返回一模一样的原始代码。
function /* foo comment */ foo () {}
foo.toString()
// "function /* foo comment */ foo () {}"
JavaScript 语言的try...catch
结构,以前明确要求catch
命令后面必须跟参数,接受try
代码块抛出的错误对象。
try {
// ...
} catch (err) {
// 处理错误
}
ES2019 做出了改变,允许catch
语句省略参数。
try {
// ...
} catch {
// ...
}
代替普通for循环,接收两个参数 (循环回调函数,this的指向)
arr.forEach(function(val, index, arr) {
console.log(val, index, arr);
//apple 0 (3) ["apple", "orange", "banana"]
//forEach.html:13 orange 1 (3) ["apple", "orange", "banana"]
//forEach.html:13 banana 2 (3) ["apple", "orange", "banana"]
console.log(this) // window 此时的this指向window
})
arr.forEach(function(val, index, arr) {
console.log(val, index, arr);
//apple 0 (3) ["apple", "orange", "banana"]
//forEach.html:13 orange 1 (3) ["apple", "orange", "banana"]
//forEach.html:13 banana 2 (3) ["apple", "orange", "banana"]
console.log(this) // Number {123} 此时的this指向传入的 123
}, 123)
// 箭头函数 forEach
arr.forEach((val, index, arr) => {
console.log(this, val, index, arr)
// this 指window,加参数不变,因为箭头函数this指向函数定义所在的对象 为windows
}, 123)
正常情况下,需要配合return
使用,返回一个新数组,如果不加return
,相当于forEach
主要用于整理后台返回数据
let arr = [
{ title: 'aaa', read: 10, hot: true },
{ title: 'bbb', read: 11, hot: true },
{ title: 'ccc', read: 12, hot: false },
{ title: 'ddd', read: 13, hot: true }
]
let newArr = arr.map((val, index, arr) => {
let json = {}
json.t = `^_^${val.title}`
json.read = val.read + 100
json.hot = val.hot == true ? '真棒' : '笨蛋'
return json;
})
console.log(newArr) // ["aaa", "bbb", "ccc", "ddd"]
用于过滤某些不符合条件的元素
let arr = [
{ title: 'aaa', read: 10, hot: true },
{ title: 'bbb', read: 11, hot: true },
{ title: 'ccc', read: 12, hot: false },
{ title: 'ddd', read: 13, hot: true }
]
let newArr = arr.filter((val, index, arr) => {
return val.hot;
})
console.log(newArr) // 返回 arr 中为true的对象
类似查找,数组中只要某一个元素符合条件,返回true
let arr = ['apple', 'orange', 'banana']
let b = arr.some((val, index, arr) => {
return val == 'banana'
})
console.log(b) // true
数组里必须每个元素都符合条件才会返回true
let arr = [1, 3, 5, 7, 9]
let a = arr.every((val, index, arr) => {
return val % 2 == 1;
})
console.log(a) // true
注意:接收的参数与其他不同
arr.reduce()
// 求数组的和
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// prev 代表前一个值,cur代表当前值
let sum = arr.reduce((prev, cur, index, arr) => {
return prev + cur
})
console.log(sum) // 55
let arr1 = [2, 2, 4]
let num = arr1.reduce((prev, cur, index, arr) => {
// return Math.pow(prev, cur) Math.pow(2, 3) 表示2的3次方
return prev**cur // ES2017新增 幂 运算符 2**3
})
console.log(num) // 256
console.log(Math.pow(2,3)) // 8
console.log(2**3) // 8
arr.reduceRight()
从右向左依次执行
let arr = [2, 2, 3] // 3的2次方的2次方
let num = arr.reduceRight((prev, cur, index, arr) => {
// return Math.pow(prev, cur)
return prev**cur
})
console.log(num) // 81
let arr = ['apple', 'orange', 'banana']
for (let val of arr) {
console.log(val) // apple orange banana
}
// arr.keys() 数组下标
for (let key of arr.keys()) {
console.log(key) // 0 1 2
}
// arr.entries() 数组某一项
for (let item of arr.entries()) {
console.log(item) // [0, "apple"] [1, "orange"] [2, "banana"]
}
for (let [value, key] of arr.entries()) {
console.log(value, key) // 0 "apple" 1 "orange" 2 "banana"
}
含义
扩展运算符是三个点(...
),将一个数组转换为用逗号分隔的参数序列
console.log(1,...[2,3,4],5); // 1,2,3,4,5
该运算符主要用于函数调用
function add(x, y) {
return x + y
}
const numbers = [4,6]
add(...numbers); // 10
替代函数的apply方法
由于扩展运算符可以展开数组,所以不需要apply
方法,将数组转为函数的参数了
// ES5
function fn(x,y,z) {
...
}
var args = [1,2,3]
fn.apply(null, args)
// ES6
function fn(x,y,z) {
...
}
var args = [1,2,3]
fn(...args)
另一个例子,通过push函数,将一个数组添加到另一个数组的尾部
// ES5
let arr1 = [0,1,2];
let arr2 = [3,4,5];
Array.prototype.push.apply(arr1, arr2);
//ES6
let arr1 = [0,1,2];
let arr2 = [3,4,5];
arr1.push(...arr2);
扩展运算符的应用
(1)、复制数组
const a1 = [1,2,3];
// 第一种写法
const a2 = [...a1];
// 第二种写法
const [...a2] = a1;
(2)、合并数组
扩展运算符提供了数组合并的新写法
const a1 = [1,2];
const a2 = [3];
const a3 = [4,5];
// ES5
a1.concat(a2, a3); // [1,2,3,4,5]
// ES6
[...a1,...a2,...a3]; // [1,2,3,4,5]
(3)、与解构赋值结合
扩展运算符可以与解构赋值结合起来,用于生成数组;如果将扩展运算符用于数组赋值,只能放在参数的最后一位
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; // []
(4)、字符串
扩展运算符还可以将字符串转换为真正的数组
[...'hello'];
// ['h','e','l','l','o']
(5)、实现了 Iterator 接口的对象
任何定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组。
let nodeList = document.querySelectorAll('div'); // nodeList不是数组,而是一个类似数组的对象。这时,扩展运算符可以将其转为真正的数组,原因就在于NodeList对象实现了 Iterator 。
let array = [...nodeList];
(6)、Map 和 Set 结构,Generator 函数
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
let arr = [...map.keys()]; // [1, 2, 3]
Generator 函数运行后,返回一个遍历器对象,因此也可以使用扩展运算符。
const go = function*(){
yield 1;
yield 2;
yield 3;
};
[...go()] // [1, 2, 3]
Array.from()
方法用于将两类对象转换为真正的数组:类似数组的对象和可遍历的对象(包括ES6新增的数据结构Set和Map)
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对象,Array.from
都可将他们转换为数组
// NodeList对象
let ps = document.querySelectorAll('p');
Array.from(ps).filter(p => {
return p.textContent.length > 100;
});
function show() {
let args = Array.from(arguments)
console.log(args) // [1, 2, 3, 4, 5]
}
show(1,2,3,4,5)
字符串转数组
let str = 'jason'
// ES5
// let strArr = str.split('');
// ES6
let strArr = Array.from(str)
console.log(strArr) // ["j", "a", "s", "o", "n"]
如果参数是一个真正的数组,Array.from()
会返回一个一模一样的数组
let arr = [1,2,3];
let arr1 = [...arr];
let arr2 = Array.from(arr)
console.log(arr2) // [1,2,3]
Array.from
方法还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有length
属性。因此,任何有length
属性的对象,都可以通过Array.from
方法转为数组,而此时扩展运算符就无法转换。
let json = {
0: 'apple',
1: 'banana',
2: 'orange'
}
let jsonArr = Array.from(json)
console.log(jsonArr) // []
let json1 = {
0: 'apple',
1: 'banana',
2: 'orange',
length: 3
}
let jsonArr1 = Array.from(json1)
console.log(jsonArr1) // ["apple", "banana", "orange"]
下面的例子将数组中布尔值为false
的成员转为0
。
Array.from([1, , 2, , 3], (n) => n || 0)
// [1, 0, 2, 0, 3]
Array.from({ length: 2 }, () => 'jack')
// ['jack', 'jack']
Array.of()
方法用于将一组值转换为数组
Array.of(1,2,3); // [1,2,3]
Array.of(); // [] 如果没有参数,就返回一个空数组
Array.of(undefined); // [undefined]
数组实例的copyWithin()
方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组,也就是说,使用这个方法,会修改当前数组
Array.prototype.copyWithin(target, start = 0, end = this.length)
它接受三个参数。
这三个参数都应该是数值,如果不是,会自动转为数值。
[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5] 从3号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2。
数组实例的find
方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有成员依次执行该回调函数,直到找出第一个返回true的成员,然后返回该成员;如果没有符合条件的成员,返回undefined
let arr = [10,30,78,69,90]
let res = arr.find((val, index, arr) => {
return val > 70;
})
console.log(res) // 78
findIndex()
方法与find()
方法类似,返回第一个符合条件的数组成员的位置,如果没有,返回-1
let arr = [10,30,78,69,90]
let idnex = arr.findIndex((val, index, arr) => {
return val > 70;
})
console.log(idnex) // 2
fill
方法使用给定值,填充一个数组
['a','b','c'].fill(7);
// [7,7,7]
new Array(3).fill(7);
// [7,7,7]
fill
还可以接收第二个和第三个参数,用于指定填充的起始位置和结束位置
let arr = new Array(10);
arr.fill('默认值', 1, 3)
console.log(arr) // [empty, "默认值", "默认值", empty × 7]
ES6 提供三个新的方法——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"
Array.prototype.includes()
方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes
相似
[1,2,3].includes(2); // true
[1,2,3].includes(0); // false
该方法的第二个参数表示搜索的起始位置,默认为0
。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4
,但数组长度为3
),则会重置为从0
开始。
[1, 2, 3].includes(3, -1); // true
没有该方法之前,我们通常使用数组的indexOf
方法,检查是否包含某个值。
indexOf
方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1
,表达起来不够直观。二是,它内部使用严格相等运算符(===
)进行判断,这会导致对NaN
的误判。
// ES5
[NaN].indexOf(NaN); // -1
// ES6
[NaN].includes(NaN); // true
另外,Map 和 Set 数据结构有一个has
方法,需要注意与includes
区分。
has
方法,是用来查找键名的,比如Map.prototype.has(key)
、WeakMap.prototype.has(key)
、Reflect.has(target, propertyKey)
。has
方法,是用来查找值的,比如Set.prototype.has(value)
、WeakSet.prototype.has(value)
。Array.prototype.flat
用于将嵌套的数组‘拉平’,变成一维的数组。该方法返回一个新数组,对原数据没有影响
[1,2,[3,4]];
// [1,2,3,4]
flat()
默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()
方法的参数写成一个整数,表示想要拉平的层数,默认为1。
[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]
如果不管有多少层嵌套,都要转成一维数组,可以用Infinity
关键字作为参数。
[1, [2, [3]]].flat(Infinity)
// [1, 2, 3]
如果原数组有空位,flat()
方法会跳过空位。
[1, 2, , 4, 5].flat()
// [1, 2, 4, 5]
flatMap()
方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()
),然后对返回值组成的数组执行flat()
方法。该方法返回一个新数组,不改变原数组
// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]
// 只能展开一层数组
flatMap()
方法的参数是一个遍历函数,该函数可以接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组。
arr.flatMap(function callback(currentValue[, index[, array]]) {
// ...
}[, thisArg])
数组的空位指,数组的某一个位置没有任何值
Array(3); // [,,,]
注意,空位不是undefined
,一个位置的值等于undefined
,依然是有值的。空位是没有任何值,in
运算符可以说明这一点。
0 in [undefined,undefined,undefined]; // true
0 in [,,,]; // false
ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位。
forEach()
, filter()
, reduce()
, every()
和some()
都会跳过空位。map()
会跳过空位,但会保留这个值join()
和toString()
会将空位视为undefined
,而undefined
和null
会被处理成空字符串。// forEach方法
[,'a'].forEach((x,i) => console.log(i)); // 1
// filter方法
['a',,'b'].filter(x => true) // ['a','b']
// every方法
[,'a'].every(x => x==='a') // true
// reduce方法
[1,,2].reduce((x,y) => x+y) // 3
// some方法
[,'a'].some(x => x !== 'a') // false
// map方法
[,'a'].map(x => 1) // [,1]
// join方法
[,'a',undefined,null].join('#') // "#a##"
// toString方法
[,'a',undefined,null].toString() // ",a,,"
ES6则是明确将空位转为undefined
Array.from
方法会将数组的空位,转为undefined
,也就是说,这个方法不会忽略空位。
Array.from(['a',,'b'])
// [ "a", undefined, "b" ]
扩展运算符(...
)也会将空位转为undefined
。
[...['a',,'b']]
// [ "a", undefined, "b" ]
copyWithin()
会连空位一起拷贝
[,'a','b',,].copyWithin(2,0) // [,"a",,"a"]
fill()
会将空位视为正常的数组位置。
new Array(3).fill('a') // ["a","a","a"]
for...of
循环也会遍历空位
let arr = [, ,];
for (let i of arr) {
console.log(1);
}
// 1
// 1
entries()
、keys()
、values()
、find()
和findIndex()
会将空位处理成undefined
。
// entries()
[...[,'a'].entries()] // [[0,undefined], [1,"a"]]
// keys()
[...[,'a'].keys()] // [0,1]
// values()
[...[,'a'].values()] // [undefined,"a"]
// find()
[,'a'].find(x => true) // undefined
// findIndex()
[,'a'].findIndex(x => true) // 0
排序稳定性(stable sorting)是排序算法的重要属性,指的是排序关键字相同的项目,排序前后的顺序不变。
const arr = [
'peach',
'straw',
'apple',
'spork'
];
const stableSorting = (s1, s2) => {
if (s1[0] < s2[0]) return -1;
return 1;
};
arr.sort(stableSorting)
// ["apple", "peach", "straw", "spork"]
ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法
let name = 'Jason';
let age = 30;
let json = {
name, // name: name,
age, // age: age
// showA: function() {
// return this.name;
// }
showA() { // 一定注意,不要使用箭头函数
return this.name
}
}
console.log(json.showA())
module.exports = { getItem, setItem, clear };
// 等同于
module.exports = {
getItem: getItem,
setItem: setItem,
clear: clear
};
注意,简写的对象方法不能用做构造函数,会报错
const obj = {
f() {
this.foo = 'bar';
}
};
new obj.f() // 报错
ES6 允许字面量定义对象时,把表达式放在方括号内。
let lastWord = 'last word';
const a = {
'first word': 'hello',
[lastWord]: 'world'
};
a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"
// 还可以用于定义方法名
let obj = {
['h' + 'ello']() {
return 'hi';
}
};
obj.hello() // hi
注意,属性名表达式与简洁表示法,不能同时使用,会报错。
// 报错
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };
// 正确
const foo = 'bar';
const baz = { [foo]: 'abc'};
函数的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"
可枚举性
对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor
方法可以获取该属性的描述对象。
let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
// {
// value: 123,
// writable: true,
// enumerable: true,
// configurable: true
// }
描述对象的enumerable
属性,称为“可枚举性”,如果该属性为false
,就表示某些操作会忽略当前属性。
目前,有四个操作会忽略enumerable
为false
的属性。
for...in
循环:只遍历对象自身的和继承的可枚举的属性。Object.keys()
:返回对象自身的所有可枚举的属性的键名。JSON.stringify()
:只串行化对象自身的可枚举的属性。Object.assign()
: 忽略enumerable
为false
的属性,只拷贝对象自身的可枚举的属性。属性的遍历
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 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()] 首先是数值属性2和10,其次是字符串属性b和a,最后是 Symbol 属性。
ES6新增了一个关键字super
,指向当前对象的原型对象
const proto = {
foo: 'hello'
};
const obj = {
foo: 'world',
find() {
return super.foo;
}
};
Object.setPrototypeOf(obj, proto); // setPrototypeOf() 设置一个指定对象的原型
obj.find() // "hello"
注意,super
关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。
// 报错
const obj = {
foo: super.foo
}
// 报错
const obj = {
foo: () => super.foo
}
// 报错
const obj = {
foo: function () {
return super.foo
}
}
const proto = {
x: 'hello',
foo() {
console.log(this.x);
},
};
const obj = {
x: 'world',
foo() {
super.foo();
}
}
Object.setPrototypeOf(obj, proto);
obj.foo() // "world"
// super.foo指向原型对象proto的foo方法,但是绑定的this却还是当前对象obj,因此输出的就是world。
解构赋值
对象的解构赋值用于从一个对象取值,相当于将目标对象自身的所有可遍历的(enumerable)、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面。
let {x, y, ...z} = {x: 1, y: 2, c: 3, d: 4}
console.log(x, y, z) // 1 2 {c: 3, d: 4} 解构赋值必须为最后一个参数,否则会报错
由于解构赋值要求等号右边是一个对象,所以如果等号右边是undefined
或null
,就会报错,因为它们无法转为对象。
let { ...z } = null; // 运行时错误
let { ...z } = undefined; // 运行时错误
注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。
let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.a.b = 2;
x.a.b // 2
扩展运算符
对象的扩展运算符(...
)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。
let json = {a: 1, b: 2}
let json2 = {...json}
delete(json2.a)
console.log(json2) // {b: 2}
console.log(json) // {a: 1, b: 2}
由于数组是特殊的对象,所以对象的扩展运算符可以用于数组
let foo = { ...['a', 'b', 'c'] };
foo
// {0: "a", 1: "b", 2: "c"}
对象的扩展运算符等同于使用Object.assign()
方法。
let aClone = { ...a };
// 等同于
let aClone = Object.assign({}, a);
编程实务中,如果读取对象内部的某个属性,往往需要判断一下该对象是否存在。比如,要读取message.body.user.firstName
,安全的写法是写成下面这样。
const firstName = (message
&& message.body
&& message.body.user
&& message.body.user.firstName) || 'default';
或者使用三元运算符?:
,判断一个对象是否存在。
const fooInput = myForm.querySelector('input[name=foo]')
const fooValue = fooInput ? fooInput.value : undefined
ES2020 引入了“链判断运算符”(optional chaining operator)?.
,简化上面的写法。
const firstName = message?.body?.user?.firstName || 'default';
const fooValue = myForm.querySelector('input[name=foo]')?.value
链判断运算符有三种用法。
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()
读取对象属性的时候,如果某个属性的值是null
或undefined
,有时候需要为它们指定默认值。常见做法是通过||
运算符指定默认值。
const headerText = response.settings.headerText || 'Hello, world!';
const animationDuration = response.settings.animationDuration || 300;
const showSplashScreen = response.settings.showSplashScreen || true;
// 但是存在一个问题,开发者的意愿是当等号左边的值为undefined或null时,才使用默认值,但是现在这种写法,等号左边的值为0或者空字符串时,也会使用默认值
为了避免这种情况,ES2020引入了新的运算符??
,它的行为类似||
,但是只有等号左边的值为undefined
或null
时,才会生效
与链判断运算符?.
一起使用
const animationDuration = response.settings?.animationDuration ?? 300
用来比较两个值是否严格相等
console.log(Object.is('foo', 'foo')); // true
console.log(NaN == NaN); // false
console.log(Number.isNaN(NaN)); // true
console.log(Object.is(NaN, NaN)) // true
console.log(+0 == -0); // true
console.log(Object.is(+0, -0)); // false
基本用法
Object.assign
方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)
let json1 = {a: 1};
let json2 = {b: 2};
let json3 = {c: 3}
// let 新的对象 = Object.assign(target, source1, source1)
let obj = Object.assign({}, json1, json2, json3)
// {a: 1, b: 2, c: 3}
注意,如果目标对象与源对象有同名属性,或者多个源有同名属性,则后面的属性会覆盖前面的属性
let json1 = {a: 1};
let json2 = {b: 2, a: 2};
let json3 = {c: 3}
let obj = Object.assign({}, json1, json2, json3)
console.log(obj) // {a: 2, b: 2, c: 3}
由于undefined
和null
无法转换成对象,所以如果他们作为参数,就会报错
Object.assign(undefined); // 报错
Object.assign(null); // 报错
如果非对象参数出现在源对象的位置,这些参数都会先转换为对象,如果无法转换成对象,则跳过,这意味着,如果undefined
和null
不在首参数,就不会报错
let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true
其他类型的值(即数组、字符串和布尔值)不在首参数,也不会报错,但是,除了字符串会以数组形式,拷贝到目标对象,其他值不会产生效果
const v1 = 'abc'
const v2 = true
const v3 = 10
const obj = Object.assign({},v1,v2,v3)
console.log(obj); // { "0": "a", "1": "b", "2": "c" }
注意点
(1)浅拷贝
Object.assign
方法实行的是浅拷贝,而不是深拷贝,也就是说,如果源对象的某个属性值是对象,那么目标对象拷贝得到的是这个对象的引用
const obj1 = {a:{b: 1}};
const obj2 = Object.assign({},obj1);
obj1.a.b = 2;
console.log(obj2.a.b); // 2
(2)同名属性的替换
遇到同名属性,Object.assign
的方法是替换,而不是添加
const target = { a: { b: 'c', d: 'e' } }
const source = { a: { b: 'hello' } }
Object.assign(target, source)
// { a: { b: 'hello' } }
(3)数组的处理
Object.assign
可以用来处理数组,但是会把数组转换为对象
Object.assign([1,2,3], [4,5]);
// 4,5,3 Object.assign把数组视为属性名为 0、1、2 的对象,因此源数组的 0 号属性4覆盖了目标数组的 0 号属性1
(4)取值函数的处理
Object.assign
只能进行值的复制,如果要复制的是一个取值函数,那么将求值后再复制
const source = {
get foo() { return 1 }
};
const target = {};
Object.assign(target, source)
// { foo: 1 }
常见用途
(1)为对象添加属性
class Point {
constructor(x, y) {
Object.assign(this, {x, y}); // 将x属性和y属性添加到Point类的对象实例
}
}
(2)为对象添加方法
Object.assign(SomeClass.prototype, {
someMethod(arg1, arg2) {
···
},
anotherMethod() {
···
}
});
// 等同于下面的写法
SomeClass.prototype.someMethod = function (arg1, arg2) {
···
};
SomeClass.prototype.anotherMethod = function () {
···
};
(3)克隆对象
function clone(origin) {
return Object.assign({}, origin);
}
(4)合并多个对象
将多个对象合并到某个对象
const merge =
(target, ...sources) => Object.assign(target, ...sources);
(5)为属性指定默认值
const DEFAULTS = {
logLevel: 0,
outputFormat: 'html'
};
function processContent(options) {
options = Object.assign({}, DEFAULTS, options);
console.log(options);
// ...
}
let {keys, values, entries} = Object
let json = {
a: 1,
b: 2,
c: 3
}
for (let key of keys(json)) { // 相当于(let key of Object.keys(json))
console.log(key) // a b c
}
for (let value of values(json)) {
console.log(value) // 1 2 3
}
for (let item of entries(json)) {
console.log(item) // ["a", 1] ["b", 2] ["c", 3]
}
for (let [key, val] of entries(json)) {
console.log(key, val) // a 1 b 2 c 3
}
Object.fromEntries
方法是Object.entries
的逆操作,用于将一个键值对数组转化为对象
Object.fromEntries([
['foo', 'bar'],
['baz', 42]
])
// { foo: "bar", baz: 42 }
Promise是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理和强大
从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理
ES6规定,Promise
对象是一个构造函数,用来生成Promise
实例
const promise = new Promise(function(resolve, reject) {
if (/*异步操作成功*/) {
resolve(value)
} else {
reject(error)
}
})
Promise
构造函数接收一个函数作为参数,该函数接收两个参数分别是resolve
和reject
Promise
实例生成后,可以用then
方法指定resolved
和rejected
状态的回调函数
promise.then(function(res) {
// success
}, function(error) {
// fail
})
// 等同于
promise.then((res) => {
// success
},(error) => {
// fail
})
下面是异步加载图片的例子
function loadImageAsync(url) {
return new Promise(function(resolve, reject) {
const image = new Image();
image.onload = function() {
resolve(image)
}
image.onerror = function() {
reject(new Error('Could not load image at ' + url))
}
image.src = url;
})
}
下面是用Promise对象实现Ajax的例子
const getJSON = function(url) {
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
});
return promise;
};
getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
});
注意,调用resolve
或reject
并不会终结Promise函数的执行
new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2
// 1
一般来说,调用resolve
或reject
以后,Promise 的使命就完成了,后继操作应该放到then
方法里面,而不应该直接写在resolve
或reject
的后面。所以,最好在它们前面加上return
语句,这样就不会有意外
new Promise((resolve, reject) => {
return resolve(1);
// 后面的语句不会执行
console.log(2);
})
Promise实例具有then
方法,then
方法的第一个参数是resolved
状态的回调函数,第二个参数(可选)是rejected
状态的回调函数
promise.then(res=> {
console.log(res)
}, err => {
console.log(err)
})
then
方法返回的是一个新的Promise
实例,因此可以采用链式写法,即then
方法后面再调用一个then
方法
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});
Promise.prototype.catch
用于指定发生错误时的回调函数
getJson('/posts.json').then(function(res) {
//
}).catch(function(error) {
console.log('发生错误' + error)
})
reject
方法的作用,等同于抛出错误
如果Promise状态已经变成resolved
,再抛出错误是无效的
const promise = new Promise(function(resolve, reject) {
resolve('ok');
throw new Error('test');
});
promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log(error) });
// ok
Promise对象的错误具有‘冒泡’属性,会一直向后传递,直到被捕获为止,也就是说,错误总是被下一个catch
捕获
getJson('/posts.json').then(function(post) {
return getJson(post.commentUrl)
}).then(function(comments) {
// ...
}).catch(function(error) {
// 处理前面3个Promise的错误 两个then和getJson
})
一般来说,不要在then
方法里面定义 Reject 状态的回调函数(即then
的第二个参数),总是使用catch
方法
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});
跟传统的try/catch
语法不同,如果没有使用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 对象后面要跟catch
方法,这样可以处理 Promise 内部发生的错误。catch
方法返回的还是一个 Promise 对象,因此后面还可以接着调用then
方法。
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
});
};
someAsyncThing()
.catch(function(error) {
console.log('oh no', error);
})
.then(function() {
console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
// carry on
// 如果没有报错,则会跳过catch
finnly
方法用于指定不管Promise对象最后状态如何,都会执行的操作
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
下面是一个例子,服务器使用Promise处理请求,然后使用finally
关掉服务器
sever.listen(port).then(function() {
// ...
}).finally(sever.stop)
finally
方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled
还是rejected
。这表明,finally
方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果
// finally 本质上是then方法的特例
promise
.finally(() => {
// 语句
});
// 等同于
promise
.then(
result => {
// 语句
return result;
},
error => {
// 语句
throw error;
}
);
finally
总是会返回原来的值
// resolve 的值是 undefined
Promise.resolve(2).then(() => {}, () => {})
// resolve 的值是 2
Promise.resolve(2).finally(() => {})
// reject 的值是 undefined
Promise.reject(3).then(() => {}, () => {})
// reject 的值是 3
Promise.reject(3).finally(() => {})
Promise.all
用于将多个Promise实例,包装成一个新的Promise实例
const p = Promise.all([p1,p2,p3])
p
的状态由p1
、p2
、p3
决定,分成两种情况。
(1)只有p1
、p2
、p3
的状态都变成fulfilled
,p
的状态才会变成fulfilled
,此时p1
、p2
、p3
的返回值组成一个数组,传递给p
的回调函数。
(2)只要p1
、p2
、p3
之中有一个被rejected
,p
的状态就变成rejected
,此时第一个被reject
的实例的返回值,会传递给p
的回调函数。
let pro1 = Promise.resolve('aaa')
let pro2 = Promise.resolve('bbb')
let pro3 = Promise.resolve('ccc')
Promise.all([pro1, pro2, pro3]).then(res => {
console.log(res) // ["aaa", "bbb", "ccc"]
let [res1, res2, res3] = res
console.log(res1, res2, res3) // aaa bbb ccc
})
注意,如果作为参数的Promise实例,自己定义了catch
方法,那么它一旦被rejected
,并不会出发Promise.all
的catch
方法
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了');
})
.then(result => result)
.catch(e => e);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 报错了]
// 如果p2没有自己的catch方法,就会调用Promise.all的catch方法
Promise.race
同样是将多个Promise实例,包装成一个新的Promise实例
const p = Promise.race([p1,p2,p3])
上面代码中,只要p1
、p2
、p3
之中有一个实例率先改变状态,p
的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p
的回调函数
下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为reject
,否则变为resolve
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);
Promise.allSettled
方法接受一组Promise实例作为参数,包装成一个新的Promise实例。只有等到这些实例都返回结果,不管是fullfilled
还是rejected
,包装实例才会结束
const promises = [
fetch('/api-1'),
fetch('/api-2'),
fetch('/api-3'),
];
await Promise.allSettled(promises);
removeLoadingIndicator(); // 对服务器发出三个请求,等到三个请求都结束,不管请求成功还是失败,加载的滚动图标就会消失。
该方法返回的新的 Promise 实例,一旦结束,状态总是fulfilled
,不会变成rejected
。状态变成fulfilled
后,Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入Promise.allSettled()
的 Promise 实例。
const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.allSettled([resolved, rejected]);
allSettledPromise.then(function (results) {
console.log(results);
});
// [
// { status: 'fulfilled', value: 42 },
// { status: 'rejected', reason: -1 }
// ]
// fulfilled时,对象有value属性,rejected时有reason属性,对应两种状态的返回值
下面是返回值用法的例子
const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];
const results = await Promise.allSettled(promises);
// 过滤出成功的请求
const successfulPromises = results.filter(p => p.status === 'fulfilled');
// 过滤出失败的请求,并输出原因
const errors = results
.filter(p => p.status === 'rejected')
.map(p => p.reason);
Promise.any()
方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只要参数实例有一个变成fulfilled
状态,包装实例就会变成fulfilled
状态;如果所有参数实例都变成rejected
状态,包装实例就会变成rejected
状态
Promise.any()
跟Promise.race()
方法很像,只有一点不同,就是不会因为某个 Promise 变成rejected
状态而结束
const promises = [
fetch('/endpoint-a').then(() => 'a'),
fetch('/endpoint-b').then(() => 'b'),
fetch('/endpoint-c').then(() => 'c'),
];
try {
const first = await Promise.any(promises);
console.log(first);
} catch (error) {
console.log(error);
}
// 只要有一个变成fulfilled,Promise.any()返回的 Promise 对象就变成fulfilled。如果所有三个操作都变成rejected,那么就会await命令就会抛出错误
Promise.resolve
方法用于将现有对象转换为Promise对象
const p = Promise.resolve($.ajax('/posts.json'))
Promise.resolve()
等价于下面的写法
Promise.resolve('foo');
// 等价于
new Promise(resolve => resolve('foo'));
Promise.resolve()
方法的参数分为四种情况
(1)参数是一个Promise实例
如果参数是Promise实例,那么Promise.resolve
将不做任何修改,原封不动返回这个实例
(2)参数是一个thenable
对象
thenable
对象指的是具有then
方法的对象,Promise.resolve
方法会将这个对象转为 Promise 对象,然后就立即执行thenable
对象的then
方法。
let thenable = {
then: function(resolve, reject) {
resolve(20)
}
}
let p1 = Promise.resolve(thenable)
p1.then((value) => {
console.log(value); // 20
})
(3)参数不是具有then
方法的对象,或根本就不是对象
如果参数是一个原始值,或者是一个不具有then
方法的对象,则Promise.resolve
返回一个新的Promise对象,状态为resolved
const p = Promise.resolve('hello');
p.then(function(res) {
console.log(res); // hello
})
(4)不带有任何参数
Promise.resolve
方法允许调用时不带参数,直接返回一个resolved
状态的Promise对象
const p = Promise.resolve();
p.then((res) => {
// ...
})
需要注意的是,立即resolve()
的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。
setTimeout(function() {
console.log('three')
},0)
Promise.resolve().then(function() {
console.log('two')
})
console.log('one')
// one
// two
// three
Promise.reject(reason)
方法也会返回一个新的Promise实例,该实例的状态为rejected
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function(err) {
console.log(err) // 出错了
})
注意,Promise.reject()
方法的参数,会原封不动地作为reject
的理由,变成后续方法的参数
const thenable = {
then(resolve, reject) {
reject('出错了');
}
};
Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
})
// true
加载图片
const loadImage = function (path) {
return new Promise(function(resolve, reject) {
const image = new Image();
image.onload = resolve();
image.onerror = reject();
image.src = path;
})
}
❓Generator 函数与 Promise 的结合
function getFoo () {
return new Promise(function (resolve, reject){
resolve('foo');
});
}
const g = function* () {
try {
const foo = yield getFoo();
console.log(foo);
} catch (e) {
console.log(e);
}
};
function run (generator) {
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);
有两种写法,让同步函数同步执行,异步函数异步执行,并且具有统一的API
第一种写法是async
函数
const f = ()=> console.log('now');
(asyns() => f())(); // f是同步的会得到同步的结果
(async() => f())() // f是异步的就可以使用`then`方法指定下一步
.then(...)
console.log('next');
// now
// next
第二种写法是使用new Promise
const f = () => console.log('now');
(
() => new Promise(
resolve => resolve(f())
)
)();
console.log('next');
// now
// next
鉴于这是一个很常见的需求,所以提供Promise.try()
方法替代上边的写法
const f = () => console.log('now')
Promise.try(f);
console.log('next')
JavaScript 一直没有模块体系,在ES6之前,社区制定了一套模块规范,主要的是CommonJS和AMD两种。前者主要用于服务端,后者用于浏览器。
// CommonJS模块
let { stat, exists, readFile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
// 实质是整体加载fs模块,生成一个对象,然后再读取这三个方法,这种加载称为‘运行时加载’,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
// ES6模块
import { stat, exists, readFile } from 'fs';
// 实质是从fs模块加载这3个方法,其他方法不加载,这种加载称为’编译时加载‘或静态加载
除了静态加载带来的各种好处,ES6 模块还有以下好处。
UMD
模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。navigator
对象的属性。Math
对象),未来这些功能可以通过模块提供。ES6的模块自动采用严格模式
严格模式主要有以下限制
with
语句delete prop
,会报错,只能删除属性delete global[prop]
eval
不会在它的外层作用域引入变量eval
和arguments
不能被重新赋值arguments
不会自动反映函数参数的变化arguments.callee
arguments.caller
this
指向全局对象fn.caller
和fn.arguments
获取函数调用的堆栈protected
、static
和interface
)模块功能主要有两个命令组成:export
和import
。export
命令用于规定模块的对外接口,import
命令用于输入其他模块提供的功能。
一个模块就是一个独立的文件。该文件内部的所有变量,外部都无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export
命令输出该变量
// profile.js
export let firstName = 'Jason';
export let lastName = 'Jie';
// 还有一种写法(优先考虑这种写法)
let firstName = 'Jason';
let lastName = 'Jie';
export {
firstName,
lastName
}
export
命令除了输出变量,还可以输出函数或类
export function sum (x, y) {
return x + y;
}
通常情况下,export
输出的变量就是本来的名字,但是可以使用as
关键字重命名
function v1() {
// ...
}
let str = 'hhh'
export {
v1 as f1,
str as s
}
需要特别注意的是,export
命令规定的是对外的接口,必须与模块内部的变量建立一对一的对应关系
// 报错
export 1;
// 报错
var m = 1;
export m;
// 报错
function f() {}
export f;
// 正确写法
// 变量
export let m = 1;
//
let m = 1;
export {m};
//
let m = 1;
export {m as n};
// 方法
export function f () {}
//
function f() {}
export {f}
另外,export
语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以获取到模块内部实时的值
export let foo = 'hhh';
setTimeout(() => {
foo = 'xxx'; // 1s后变为xxx
}, 1000)
使用export
命令定义了模块的对外接口后,其他JS文件就可以通过import
命令加载这个模块了
// 大括号里的变量名,必须与被导入模块对外接口的名称相同
import {firstName, lastName} from './profile.js'
// 使用
function setName(element) {
element.textContent = firstName + ' ' + lastName;
}
如果想为输入的变量重新起名字,import
命令要使用as
关键字,将输入的变量重名
import {lastName as surName} from './profile.js'
import
输入的变量都是只读的,也就是说,不允许在加载模块的脚本里,改写接口,但是改写对象的属性是允许的
// 变量
import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
// 对象
import {a} from './xxx.js'
a.foo = 'hello'; // 合法操作,由于此写法很难查错,建议凡是输入的变量,都当作完全可读,不要轻易改变它的属性
import
后边的from
指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js
文件名可省略
注意,import
命令具有提升效果,会提升到整个模块的顶部,首先执行
foo();
import { foo } from './profile';
由于import
是静态执行,所以不能使用变量和表达式
// 报错
import { 'f' + 'oo' } from 'my_module';
// 报错
let module = 'my_module';
import { foo } from module;
// 报错
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
如果多次重复执行同一条import
语句,那么只会执行一次,不会执行多次
// 只执行一次
import 'lodash';
import 'lodash';
除了指定加载某个输出值,还可以使用整体加载,即用星号*
指定一个对象,所有输出值都加载在这个对象上
// circle.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
// 加载模块
// main.js
import * as circle from './circle.js';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14))
从上面可以看出,使用import
命令的时候,用户必须要知道所要加载的变量名或者函数名,对于用户来说不方便,为了使他们不阅读文档就能加载模块,就要用到export default
命令,为模块指定默认输出。
// export-default.js
export default function() {
console.log('foo');
}
// 其他模块加载该模块时,import 命令可以为该模块指定任意名字
import customName from './export-default.js'; // 需要注意的是,这时import 后边,不需要大括号
customName(); // foo
export default
用在非匿名函数前,也是可以的
export default function foo() {
// ...
}
// 或者
function foo() {
// ...
}
export default foo;
下面比较一下默认输出和正常输出
// 默认输出
export default function foo() {
// ...
}
import foo from 'foo';
// 正常输出
export function foo() {
// ...
}
import {foo} from 'foo';
export default
命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default
命令只能使用一次。所以,import
后边才不用加大括号,因为只可能唯一对应export default
命令。
本质上,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 b = 10;
export default b;
// 错误
export default var c = 8;
// 正确
export default 7;
// 报错
export 7
如果想在一条import
语句中,同时输入默认方法和其他接口,可以写成下面这样
import _, {foo, bar} from 'xxx';
// 对应如下
export default function() {
// ...
}
export function foo() {}
export function bar() {}
export default
也可以用来输出类
// MyClass.js
export default class { ... }
// main.js
import MyClass from 'MyClass';
let o = new MyClass();
如果在一个模块中,先输入后输出同一个模块,import
语句可以与export
语句写在一起
export {foo, bar} from './profile.js';
// 可以简单理解为
import {foo, bar} from './profile.js';
export {foo, bar};
// 需要注意的是,写成一行后,foo 和 bar实际上并没有被导入到当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo 和 bar
// 接口改名
export { foo as myFoo } from 'my_module';
// 整体输出
export * from 'my_module';
// 默认接口
export {default} from 'my_module';
// 具名接口改为默认接口的写法
export { es6 as default } from './someModule';
// 等同于
import { es6 } from './someModule';
export default es6;
// 默认接口也可以改为具名接口
export { default as es6 } from './someModule';
模块之间也可以继承
假设有一个circleplus
模块,继承了circle
模块。
// circleplus.js
export * from 'circle'; // export * 命令会忽略circle里的默认方法
export var e = 2.71828182846;
export default function(x) {
return Math.exp(x);
}
这时,也可以将circle
的属性或方法,改名后再输出
// circleplus.js
export { area as circleArea } from 'circle';
加载上面模块的写法如下
// main.js
import * as math from 'circleplus';
import exp from 'circleplus';
console.log(exp(math.e));
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';
简介
前面介绍过,import
命令会被 JavaScript 引擎静态分析,先于模块内的其他语句执行(import
命令叫做“连接” binding 其实更合适)。所以,下面的代码会报错。
// 报错
if (x === 2) {
import MyModual from './myModual';
}
import()
函数,支持动态加载模块。
import('./modules/1.js').then((res) => {
console.log(res)
})
import()
返回一个Promise对象
const main = document.querySelector('main');
import(`./section-modules/${someVariable}.js`)
.then(module => {
module.loadPageInto(main);
})
.catch(err => {
main.textContent = err.message;
});
适用场合
(1)按需加载
import()
可以在需要的时候,再加载某个模块
button.addEventListener('click', event => {
import('./dialogBox.js')
.then(dialogBox => {
dialogBox.open();
})
.catch(error => {
/* Error handling */
})
});
(2)条件加载
import()
可以放在if
代码块,根据不同的情况,加载不同的模块
if (condition) {
import('moduleA').then(...);
} else {
import('moduleB').then(...);
}
(3)动态的模块路径
import()
允许模块路径动态生成
import(f())
.then(...);
注意点
import()
加载模块成功以后,这个模块会作为一个对象,当作then
方法的参数。因此,可以使用对象解构赋真的方法,获取输出接口。
import('xxx.js').then(({export1, export2}) => {
console.log(export1, export2)
})
如果模块有default
输出接口,可以用参数直接获得
import('xxx.js').then((res) => {
console.log(res.default)
})
也可以使用具名输入的形式
import('xxx.js').then(({default: theDefault}) => {
console.log(theDefault)
})
如果想同时加载多个模块,可以采用下面的方法
Promise.all([
import('./modules/1.js'),
import('./modules/2.js')
]).then(([mod1, mod2]) => {
console.log(mod1, mod2)
})
import()
也可用在async
函数之中
async function main() {
const modOne = await import('./modules/1.js');
const modTwo = await import('./modules/2.js');
console.log(modOne, modTwo);
// 等同于
const [m1, m2] = await Promise.all([
import('./modules/1.js'),
import('./modules/2.js')
])
console.log(m1, m2)
}
main()
传统方法
HTML网页中,浏览器通过标签加载Javascript脚本
<!-- 页面内嵌的脚本 -->
<script type="application/javascript">
// module code
</script>
<!-- 外部脚本 -->
<script type="application/javascript" src="path/to/myModule.js">
</script>
// 默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到