文章目录
let
1.具有块级作用域
单纯打括号都算是块级作用域
ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。块级作用域只影响变量 跟let 函数还是没有块级作用域derrr
块级作用域还引申出一个东西 就是var全局声明是共用一个i 但是let是有多个i for循环每次执行的i都是不一样的i for循环用了很多个i 每次都是单独声明单独使用
这个for循环做了3次 声明了3个不一样的i(有3个i哦)
2.不可以重复命名
暂时性死区没太理解 为什么会知道后面有let呢 明明没有变量提升啊。。先输出后let定义 出的是没初始化
3.没有变量提升
const
const是不修改变量内容,变量存的是数组、对象的地址值,只要不修改变量的指向,修改数组、对象里面的内容应该是没什么关系的。
变量的解构赋值
换个东西绑定了 记得带解构数组方括号,解构对象带花括号捏!
解构不仅可以用于数组,还可以用于对象。
let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined
上面代码的第一个例子,等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。第二个例子的变量没有对应的同名属性,导致取不到值,最后等于undefined
。
如果解构失败,变量的值等于undefined
。
let {foo} = {bar: 'baz'};
foo // undefined
上面代码中,等号右边的对象没有foo
属性,所以变量foo
取不到值,所以等于undefined
。
模板字符串
对象的简写
可以直接写变量放进去
还可以在里面简写方法:
箭头函数
箭头函数本质上是匿名函数 return语句
因为没有使用function关键字声明 没有自己的作用域 所以没有自己的专属this 只能依靠父级的this 箭头函数定义时的父级 如果直接使用箭头函数来call() apply() bind() 箭头函数的父级this没变 所以是不会变的 箭头函数的this是指这种
1.箭头函数的this是静态的 不会改变 指使用bind() call() apply()不变
我觉得就因为箭头函数的this是静态的所以用箭头函数不可以作为构造函数使用new调用 new是要改变this,来初始化对象的 箭头函数做不到捏
函数参数默认值
rest参数
rest获取实参的列表 并转化成数组 感觉很像以前c++里面的可变参数函数 就是那种参数数组 也是要放到最后面的
因为rest获取的是参数数组 所以可以直接用数组的函数haha
扩展运算符
扩展运算符(spread)是三个点(…)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。
应用:
1.rest参数
2.拆数组 转数组(Map Set)
//真的转化成了用逗号分隔的参数序列
console.log(1,2,3,4)//1 2 3 4
console.log(...[1, 2, 3])
// 1 2 3
console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5
[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]
就是拆开数组,将数组变成一个一个的参数序列,它们之间用逗号分隔开。
1.比如例子1 第三行是“王太利”,“肖央”+“曾毅”,“玲花” 先拆开再组合这样
2.数组的克隆是浅复制 只是简单的复制了一下数组地址
3.
可以作为函数参数,但是要放在最后面,就跟以前C++的数组参数(可变长度参数?)一样,作为函数参数的时候,它叫rest参数。
function push(array, ...items) {
array.push(...items);
}
function add(x, y) {
return x + y;
}
const numbers = [4, 38];
add(...numbers) // 42
如果不放在最后就会报错
let test=(...args,a)=>{//Uncaught SyntaxError: Rest parameter must be last formal parameter
console.log(...args+a)
}
test([1,2,3,4],5)
但是调用的时候好随便。。。
function f(v, w, x, y, z) { }
const args = [0, 1];
f(-1, ...args, 2, ...[3]);//是能过的哈 就是将数组拆开了,拆成0根1两个参数了。
Symbol
Symbol.for()与Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol是一个动态值。
括号里面的值只是给Symbol做注释而已,不会干扰Symbol的产生的值 就算注释一样值也可以不一样
s2 s3是不一样的 s4 s5是一样的
Symbol会一直注册新值,就算使用一样的参数去创建新值也会返回新的。Symbol.for就不会,创建新值之前会全局搜索,已经有过的不会再创建,创建新值是全局注册的。
不能被for循环找到
Symbol 作为属性名,遍历对象的时候,该属性不会出现在for…in、for…of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。
Symbol.isConcatSpreadable() 就是在拼接数组的时候数组能不能拆开 原来数组对象里面是没有这个属性的(如91行)
当设置成false的时候就是不能拆开,要作为一个整体加进去
只是打包 跟放头尾没关系 设置成false也可以放头
类数组对象正好相反
补充类数组对象:
javascript定义类数组对象的方法是:1、首先创建一个空对象;2、为对象直接定义数字下标的属性;3、关键点,为对象设置length属性和splice属性为数字和函数。
迭代器
(js高程)迭代器并不与可迭代对象某个思科的快照绑定,而仅仅是使用游标记录遍历可迭代对象的历程。
for…of是取的值 for…in 是取的键
const banji = {
name: "aa",
stus: ["st1", "st2", "st3", "st4", "st5", "st6"],
[Symbol.iterator]() {
let index = 0;
const this_ = this;
return {
next: function () {
if (index < this_.stus.length) {
console.log(this_.stus.length)
const result = { value: this_.stus[index], done: false };//value是迭代器返回的值 done是表示迭代器状态 true是一件完成了 然后就会跳出循环 false就会继续循环 再调用next()
index++;
console.log(result)
return result;
} else {
return { value: "undefined", done: true };
}
},
};
},
};
for (let i of banji) {
console.log(i);
}
例子2:
const obj = {
[Symbol.iterator] : function () {
return {
next: function () {
return {
value: 1,
done: true
};
}
};
}
};
对象obj
是可遍历的(iterable),因为具有Symbol.iterator
属性。执行这个属性,会返回一个遍历器对象。该对象的根本特征就是具有next
方法。每次调用next
方法,都会返回一个代表当前成员的信息对象,具有value
和done
两个属性。
我的理解:对象能遍历=对象具有Symbol.iterator
属性=执行Symbol.iterator
属性时能返回遍历器对象=能返回带next
方法的对象(原型链上带next
也行 =》只要有Symbol.iterator
属性+next
方法就ok(直接给Symbol.iterator
属性实现函数也行)
下面这个例子是类部署 Iterator 接口的写法
class RangeIterator {
constructor(start, stop) {
this.value = start;
this.stop = stop;
}
[Symbol.iterator]() { return this; }
next() {
var value = this.value;
if (value < this.stop) {
this.value++;
return {done: false, value: value};
}
return {done: true, value: undefined};
}
}
function range(start, stop) {
return new RangeIterator(start, stop);
}
for (var value of range(0, 3)) {
console.log(value); // 0, 1, 2
}
Generator(迭代器生成函数)
阮一峰这个表述我觉得还蛮微妙。
Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态
我感觉就是能够被迭代的函数,相当于用yield给函数分成一块一块的(也就是分成不同的状态),调用一次next()执行一块(状态可能发生变更)。函数也是一种对象,Generator
是其中能迭代的那种。虽然每次执行Generator
都会生成一个遍历器对象,但是Generator
并不能作为构造函数使用new
调用,执行时的this也不指向遍历器对象。
注意:箭头函数不能拿来定义生成器函数。
我感觉是生成器函数需要生成遍历器对象,箭头没有自己的this
。
yield语句只能在Generator函数里面,在普通函数里面没有用。
执行 Generator 函数会返回一个遍历器对象,返回的遍历器对象
,但Generator函数并不开始执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象(遍历器对象),要等到遍历器对象调用next()函数才开始执行。
遍历器对象可以依次遍历 Generator 函数内部的每一个状态。遍历器对象跟上面的一模一样,也是value+done属性。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
//调用Generator之后并不执行 而是返回一个迭代器对象 迭代器的next才是函数执行的启动器
var hw = helloWorldGenerator();//返回一个迭代器对象 可以用这个迭代器来进行迭代 支持for of
console.log(hw.next())//第一次next()就是Generator函数的启动器hh
// { value: 'hello', done: false }
console.log(hw.next())
// { value: 'world', done: false }
console.log(hw.next())
// { value: 'ending', done: true }
console.log(hw.next())//删了return value就变成undefined
// { value: undefined, done: true }
遍历器对象的next方法的运行逻辑如下(其实跟上面的遍历器的逻辑是一样的):
(1)遇到yield
表达式,就暂停执行后面的操作,并将紧跟在yield
后面的那个表达式的值,作为返回的对象的value
属性值。(这里后面被包装成 async
跟await
了)
(2)下一次调用next
方法时,再继续往下执行,直到遇到下一个yield
表达式。
(3)如果没有再遇到新的yield
表达式,就一直运行到函数结束,直到return
语句为止,并将return
语句后面的表达式的值,作为返回的对象的value
属性值。
(4)如果该函数没有return
语句,则返回的对象的value
属性值为undefined
。
(3)、(4)意思是将return也看成一段,而且Generator本来就是函数,没有return就是返回undefined的。遍历器对象的next
方法的运行逻辑如下。
function* helloWorldGenerator() {
yield 'hello';//第一次调用next() 遇到yield停下 将紧跟在yield后面的值返回作为yield表达式的返回值 所以返回'hello'
yield 'world';
return 'ending';
}
yield
表达式的使用
1.yield
表达式如果用在另一个表达式之中,必须放在圆括号里面。
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}
2.yield
表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
function* demo() {
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
}
next()参数
这里主要是next传入的参数将
上一个
yield的返回值给覆盖了。
(js高程)上一次让生成器函数暂停的yield关键字会接收到传给next()方法的第一个值。
function* f() {
for (var i = 0; true; i++) {
var reset = yield i;
console.log(reset)
if (reset) {
i = -1;
}
}
}
var g = f();
console.log(g.next()); // { value: 0, done: false }
console.log(g.next()); // { value: 1, done: false }
console.log(g.next(true)); // { value: 0, done: false }
yield在赋值的中间
第一个next
从函数开始走到yield那行,走到变量初始化但是还没来得及赋值就被yield暂停了 然后返回退出函数
第二次从赋值开始走,上一个yield的返回值是undefined(那个遍历器对象是next
函数的返回值嗷)所以打印的reset是undefined
第三个yield里面的next赋值了,覆盖了上一个yield的值 所以第三次打印的就是那个true了
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
可以看出来a,b是两块单独的执行域吧
上面代码中,第二次运行next方法的时候不带参数,导致 y 的值等于2 * undefined(即NaN)
,除以 3 以后还是NaN,因此返回对象的value属性也等于NaN。
执行b的时候不是覆盖里面的变量是覆盖yield后面的全部(毕竟是覆盖yield的返回值
由于next
方法的参数表示上一个yield
表达式的返回值,所以在第一次使用next
方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next
方法时的参数,只有从第二次使用next
方法开始,参数才是有效的。从语义上讲,第一个next
方法用来启动遍历器对象,所以不用带有参数。
再看一个通过next
方法的参数,向 Generator 函数内部输入值的例子。
function* dataConsumer() {
console.log('Started');
console.log(`1. ${yield}`);
console.log(`2. ${yield}`);
return 'result';
}
let genObj = dataConsumer();
genObj.next();
// Started
genObj.next('a')
// 1. a
genObj.next('b')
// 2. b
上面代码是一个很直观的例子,每次通过next
方法向 Generator 函数输入值,然后打印出来。
这个例子表现了一句话,就是next参数就是上次yield
表达式的返回值,yield
是在那里停下,然后yield
后面的值是next
函数的返回值!不是yield
的!!
如果想要第一次调用next
方法时,就能够输入值,可以在 Generator 函数外面再包一层。
function wrapper(generatorFunction) {
return function (...args) {
let generatorObject = generatorFunction(...args);
generatorObject.next();//在这里包了一个next() 其实就是next() 提前了
return generatorObject;
};
}
const wrapped = wrapper(function* () {
console.log(`First input: ${yield}`);
return 'DONE';
});
wrapped().next('hello!')
// First input: hello!
上面代码中,Generator 函数如果不用wrapper
先包一层,是无法第一次调用next
方法,就输入参数的。
Symbol.iterator
任意一个对象的Symbol.iterator
方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。
由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator
属性,从而使得该对象具有 Iterator 接口。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
上面代码中,Generator 函数赋值给Symbol.iterator
属性,从而使得myIterable
对象具有了 Iterator 接口,可以被...
运算符遍历了(也可以使用for...of
遍历 ...
扩展运算符实际是用for...of
实现的)
Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator
属性,执行后返回自身。
function* gen(){
// some code
}
var g = gen();
g[Symbol.iterator]() === g
// true
上面代码中,gen
是一个 Generator 函数,调用它会生成一个遍历器对象g
。它的Symbol.iterator
属性,也是一个遍历器对象生成函数,执行后返回它自己。
所以可以给对象加上这个属性,来给对象加上一个遍历器接口 让对象可以使用for…of
for...of
循环可以自动遍历 Generator 函数运行时生成的Iterator
对象,且此时不再需要调用next
方法。(因为for...of
就是在消耗迭代器对象呀)
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
上面代码使用for...of
循环,依次显示 5 个yield
表达式的值。这里需要注意,一旦next
方法的返回对象的done
属性为true
,for...of
循环就会中止,且不包含该返回对象,所以上面代码的return
语句返回的6
,不包括在for...of
循环之中。
补充:
for…of
可以遍历遍历器对象 支持自定义对象的遍历器接口 对象进行for…of循环时,会调用Symbol.iterator方法,返回该对象的默认遍历器。
这篇写蛮好:
https://blog.csdn.net/w390058785/article/details/80522383
var arr = ['nick','freddy','mike','james'];
for(var item of arr){
console.log(item);
}
总结:
1.for…of遍历的是遍历器对象的值 遍历器对象({value: ,done:})
2.for…of不支持遍历对象,以及数组的自定义属性;
for…in
var userMsg = {
nick: {
name: "nick",
age: 18,
sex: "男",
},
freddy: {
name: "freddy",
age: 24,
sex: "男",
},
};
for (var key1 in userMsg) {
console.log(key1);
for (var key2 in userMsg[key1]) {
console.log(key1 + ": " + userMsg[key1][key2]);
}
}
// nick;
// nick: nick;
// nick: 18;
// nick: 男;
// freddy;
// freddy: freddy;
// freddy: 24;
// freddy: 男;
这2个例子说明for…in可以用来遍历对象.
一点对数组的思考 不知道补在哪里 随便写在这里好了
其实对象就是键值对的集合,键是属性名,值是属性值。数组是特殊的Object,它也是由很多个键值对组成的,键是数组下标,值是数组的值。然后数组的键名居然是字符串而不是Number,我震惊。。。(JavaScript 语言规定,对象的键名一律为字符串,所以,数组的键名其实也是字符串。之所以可以用数值读取,是因为非字符串的键名会被转为字符串。)
这是因为for...in
是用用来遍历对象的,对象确实是键名为字符串,用for...in
来遍历数组的时候是将数组当对象去遍历的,所以会将数组的数字索引转换为字符串= =
Generator.prototype.throw()
Generator 函数返回的遍历器对象,都有一个throw
方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
var g = function* () {
try {
yield;
} catch (e) {
console.log('内部捕获', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b
上面代码中,遍历器对象i
连续抛出两个错误。第一个错误被 Generator 函数体内的catch
语句捕获。i
第二次抛出错误,**由于 Generator 函数内部的catch
语句已经执行过了,不会再捕捉到这个错误了,**所以这个错误就被抛出了 Generator 函数体,被函数体外的catch
语句捕获。
throw
方法可以接受一个参数,该参数会被catch
语句接收,建议抛出Error
对象的实例。
var g = function* () {
try {
yield;
} catch (e) {
console.log(e);
}
};
var i = g();
i.next();
i.throw(new Error('出错了!'));
// Error: 出错了!(…)
注意,不要混淆遍历器对象的throw
方法和全局的throw
命令。上面代码的错误,是用遍历器对象的throw
方法抛出的,而不是用throw
命令抛出的。后者只能被函数体外的catch
语句捕获
var g = function* () {
while (true) {
try {
yield;
} catch (e) {
if (e != 'a') throw e;//这里throw了 错误直接被抛出就不往下走了
console.log('内部捕获', e);
}
}
};
var i = g();
i.next();
try {
throw new Error('a');
throw new Error('b');
} catch (e) {
console.log('外部捕获', e);
}
// 外部捕获 [Error: a]
上面代码之所以只捕获了a
,是因为函数体外的catch
语句块,捕获了抛出的a
错误以后,就不会再继续try
代码块里面剩余的语句了。
如果 Generator 函数内部没有部署try...catch
代码块,那么throw
方法抛出的错误,将被外部try...catch
代码块捕获。
var g = function* () {
while (true) {
yield;//因为函数内部没有try...catch块,所以错误直接被抛出
console.log('内部捕获', e);
}
};
var i = g();
i.next();
try {
i.throw('a');//错误被抛出到这里,然后被下面的catch块捕获
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
// 外部捕获 a
上面代码中,Generator 函数g
内部没有部署try...catch
代码块,所以抛出的错误直接被外部catch
代码块捕获。
如果 Generator 函数内部和外部,都没有部署try...catch
代码块,那么程序将报错,直接中断执行。
var gen = function* gen(){
yield console.log('hello');
yield console.log('world');
}
var g = gen();
g.next();
g.throw();
// hello
// Uncaught undefined
上面代码中,g.throw
抛出错误以后,没有任何try...catch
代码块可以捕获这个错误,导致程序报错,中断执行。
throw
方法抛出的错误要被内部捕获,前提是必须至少执行过一次next
方法。
function* gen() {
try {
yield 1;
} catch (e) {
console.log('内部捕获');
}
}
var g = gen();
g.throw(1);
// Uncaught 1
上面代码中,g.throw(1)
执行时,next
方法一次都没有执行过。这时,抛出的错误不会被内部捕获,而是直接在外部抛出,导致程序出错。这种行为其实很好理解,因为第一次执行next
方法,等同于启动执行 Generator 函数的内部代码,否则 Generator 函数还没有开始执行,这时throw
方法抛错只可能抛出在函数外部。
throw
方法被捕获以后,会附带执行下一条yield
表达式。也就是说,会附带执行一次next
方法。
var gen = function* gen(){
try {
yield console.log('a');
} catch (e) {
// ...
}
yield console.log('b');
yield console.log('c');
}
var g = gen();
g.next() // a
g.throw() // b
g.next() // c
上面代码中,g.throw
方法被捕获以后,自动执行了一次next
方法,所以会打印b
。另外,也可以看到,只要 Generator 函数内部部署了try...catch
代码块,那么遍历器的throw
方法抛出的错误,不影响下一次遍历。
(通过这个例子我发现yield
返回的表达式还会执行一下)
Generator 函数体内抛出的错误,也可以被函数体外的catch
捕获。
function* foo() {
var x = yield 3;
var y = x.toUpperCase();
yield y;
}
var it = foo();
it.next(); // { value:3, done:false }
try {
it.next(42);
} catch (err) {
console.log(err);
}
上面代码中,第二个next
方法向函数体内传入一个参数 42,数值是没有toUpperCase
方法的,所以会抛出一个 TypeError 错误,被函数体外的catch
捕获。
一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用next
方法,将返回一个value
属性等于undefined
、done
属性等于true
的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。
function* g() {
yield 1;
console.log('throwing an exception');
throw new Error('generator broke!');
yield 2;
yield 3;
}
function log(generator) {
var v;
console.log('starting generator');
try {
v = generator.next();
console.log('第一次运行next方法', v);
} catch (err) {
console.log('捕捉错误', v);
}
try {
v = generator.next();
console.log('第二次运行next方法', v);
} catch (err) {
console.log('捕捉错误', v);
}
try {
v = generator.next();
console.log('第三次运行next方法', v);
} catch (err) {
console.log('捕捉错误', v);
}
console.log('caller done');
}
log(g());
// starting generator
// 第一次运行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉错误 { value: 1, done: false } 因为没有yield新的值,没有覆盖,所以还是那个遍历器对象
// 第三次运行next方法 { value: undefined, done: true }
// caller done
上面代码一共三次运行next
方法,第二次运行的时候会抛出错误,然后第三次运行的时候,Generator 函数就已经结束了,不再执行下去了。
Generator.prototype.return()
Generator 函数返回的遍历器对象,还有一个return()
方法,可以返回给定的值,并且终结遍历 Generator 函数。
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }
上面代码中,遍历器对象g
调用return()
方法后,返回值的value
属性就是return()
方法的参数foo
。并且,Generator 函数的遍历就终止了,返回值的done
属性为true
,以后再调用next()
方法,done
属性总是返回true
。
如果return()
方法调用时,不提供参数,则返回值的value
属性为undefined
。
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return() // { value: undefined, done: true }
如果 Generator 函数内部有try...finally
代码块,且正在执行try
代码块,那么return()
方法会导致立刻进入finally
代码块,执行完以后,整个函数才会结束。
function* numbers () {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }
上面代码中,调用return()
方法后,就开始执行finally
代码块,不执行try
里面剩下的代码了,然后等到finally
代码块执行完,再返回return()
方法指定的返回值。
补充:js中的catch方法
throw
语句用来抛出一个用户自定义的异常。当前函数的执行将被停止(throw
之后的语句将不会执行),并且控制将被传递到调用堆栈中的第一个catch
块。如果调用者函数中没有catch
块,程序将会终止。
next()、throw()、return() 的共同点
next()
、throw()
、return()
这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield
表达式。
next()
是将yield
表达式替换成一个值。
const g = function* (x, y) {
let result = yield x + y;
return result;
};
const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}
gen.next(1); // Object {value: 1, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = 1;
上面代码中,第二个next(1)
方法就相当于将yield
表达式替换成一个值1
。如果next
方法没有参数,就相当于替换成undefined
。
throw()
是将yield
表达式替换成一个throw
语句。
gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));
return()
是将yield
表达式替换成一个return
语句。
gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;
yield* 表达式
ES6 提供了yield*
表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数。
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
//等同于
function* bar() {
yield 'x';
// 手动遍历 foo()
for (let i of foo()) {
console.log(i);
}
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// x
// a
// b
// y
从语法角度看,如果yield
表达式后面跟的是一个遍历器对象,需要在yield
表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为yield*
表达式。
let delegatedIterator = (function* () {
yield 'Hello!';
yield 'Bye!';
}());
let delegatingIterator = (function* () {
yield 'Greetings!';
yield* delegatedIterator;
yield 'Ok, bye.';
}());
for(let value of delegatingIterator) {
console.log(value);
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."
上面代码中,delegatingIterator
是代理者,delegatedIterator
是被代理者。由于yield* delegatedIterator
语句得到的值,是一个遍历器,所以要用星号表示。运行结果就是使用一个遍历器,遍历了多个 Generator 函数,有递归的效果。(读到这里突然想起设计模式了!有时间去看看)
yield*
后面的 Generator 函数(没有return
语句时),等同于在 Generator 函数内部,部署一个for...of
循环。
function* concat(iter1, iter2) {
yield* iter1;
yield* iter2;
}
// 等同于
function* concat(iter1, iter2) {
for (var value of iter1) {
yield value;
}
for (var value of iter2) {
yield value;
}
}
上面代码说明,yield*
后面的 Generator 函数(没有return
语句时),不过是for...of
的一种简写形式,完全可以用后者替代前者。反之,在有return
语句时,则需要用var value = yield* iterator
的形式获取return
语句的值(有return
,yield*
实际返回的就是return
的值)
function* concat(iter1, iter2) {
let a= yield* iter1;
// console.log(a);
let b= yield* iter2;
// console.log(b);
}
function* testa(){
yield console.log(1);
yield console.log(2);
return console.log( "testreturn A");
}
function* testb(){
yield console.log(3);
yield console.log(4);
return console.log( "testreturn B");
}
let g=concat(testa(),testb())
g.next()
g.next()
g.next()
g.next()
g.next()
console.log("run");
// 1
// 2
// testreturn A
// 3
// 4
// testreturn B
// run
//return跟其他的yield也没什么太大差别 也会执行的
不过如果yield*
有return
想获得return
值就用一个接收一下就ok
如果yield*
后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。
function* gen(){
yield* ["a", "b", "c"];
}
gen().next() // { value:"a", done:false }
上面代码中,yield
命令后面如果不加星号,返回的是整个数组,加了星号就表示返回的是数组的遍历器对象。
实际上,任何数据结构只要有 Iterator 接口,就可以被yield*
遍历。
如果被代理的 Generator 函数有return
语句,那么就可以向代理它的 Generator 函数返回数据。(说实话,这句话懂了,但是下面例子没懂 我觉得应该return停一下
function* foo() {
yield 2;
yield 3;
return "foo";
}
function* bar() {
yield 1;
var v = yield* foo();
console.log("v: " + v);
yield 4;
}
var it = bar();
it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}
function* genFuncWithReturn() {
yield 'a';
yield 'b';
return 'The result';
}
function* logReturned(genObj) {
let result = yield* genObj;
console.log(result);
}
[...logReturned(genFuncWithReturn())]
// The result
// 值为 [ 'a', 'b' ]
这两个例子都是return
没有当成一次暂停,就直接跑到下一次的yield
了。
上面代码中,存在两次遍历。第一次是扩展运算符遍历函数logReturned
返回的遍历器对象,第二次是yield*
语句遍历函数genFuncWithReturn
返回的遍历器对象。这两次遍历的效果是叠加的,最终表现为扩展运算符遍历函数genFuncWithReturn
返回的遍历器对象。所以,最后的数据表达式得到的值等于[ 'a', 'b' ]
。但是,函数genFuncWithReturn
的return
语句的返回值The result
,会返回给函数logReturned
内部的result
变量,因此会有终端输出。
就是...
运算符去找函数logReturned
的遍历器对象返回的,用的是yield*
语句函数genFuncWithReturn
返回的遍历器对象(两次yield
返回了遍历器对象,函数return
返回的是字符串'The result'
。
作为对象属性的 Generator 函数
如果一个对象的属性是 Generator 函数,可以简写成下面的形式。
let obj = {
* myGeneratorMethod() {
···
}
};
上面代码中,myGeneratorMethod
属性前面有一个星号,表示这个属性是一个 Generator 函数。
它的完整形式如下,与上面的写法是等价的。
let obj = {
myGeneratorMethod: function* () {
// ···
}
};
Generator 函数的this
Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的prototype
对象上的方法。
function* g() {}
g.prototype.hello = function () {
return 'hi!';
};
let obj = g();
obj instanceof g // true
obj.hello() // 'hi!'
上面代码表明,Generator 函数g
返回的遍历器obj
,是g
的实例,而且继承了g.prototype
。但是,如果把g
当作普通的构造函数,并不会生效,因为**g
返回的总是遍历器对象**,而不是this
对象。
function* g() {
this.a = 11;
}
let obj = g();
obj.next();
obj.a // undefined
上面代码中,Generator 函数g
在this
对象上面添加了一个属性a
,但是obj
对象拿不到这个属性。
Generator 函数也不能跟new
命令一起用,会报错。
Generator 与上下文
Generator 函数不是这样,它执行产生的上下文环境,一旦遇到yield
命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对它执行next
命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行。
function* gen() {
yield 1;
return 2;
}
let g = gen();
console.log(
g.next().value,
g.next().value,
);
上面代码中,第一次执行g.next()
时,Generator 函数gen
的上下文会加入堆栈,即开始运行gen
内部的代码。等遇到yield 1
时,gen
上下文退出堆栈,内部状态冻结。第二次执行g.next()
时,gen
上下文重新加入堆栈,变成当前的上下文,重新恢复执行。
Promise
只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。而且Promise的状态改变是一次性的,不能改两次,这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
补充:
resolve指的是Promise对象状态已经改变过了已经定型了,并不指fulfilled或rejected状态(这两个指标是平行der)
因为Promise是构造函数,所以一旦开始就不能停下了。
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
在resolve跟reject函数里面传参就是PromiseResult的属性值(是初始化还是?)
补充:
Promise.then
then在Promise原型上,函数的参数为两个函数,第一个参数接收到的函数会在p这个Promise对象为fulfilled的时候调用,即成功时调用,第二个参数接收到的函数会在Promise对象为reject时调用,即失败时调用。这两个函数都是可选的,不一定要提供。它们都接受Promise对象传出的值作为参数。then调用函数后返回的对象仍是Promise对象,也因此Promise.then支持链式调用。
重点:只有在Promise对象状态改变的时候,then里面的函数才会执行,再根据Promise对象变成fulfilled或者rejected执行第一个或执行第二个。如果Promise对象没有改变的话(Promise对象状态为pending),then不会调用第一or第二个参数der。
只写一个也行。
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, "done");//"done"作为resolve函数的参数
});
}
timeout(100).then((value) => {
console.log("value "+value);//里面这个箭头函数是resolve函数的函数声明
});
//value done
//Promise的resolve跟reject都是函数 本来很疑惑函数声明在在哪的
// 原来是在then里面
const p = new Promise((resolve, reject) => {
resolve("成功的结果");//reslove的函数调用
});
// then的两个参数都是函数 第一个函数的对象是
p.then(
(value) => {//value就是形参
console.log(value)//相当于是resolve的函数声明
},
(reason) => {
console.log(reason)//相当于是reject的函数声明
}
);//打印"成功的结果"
then的执行顺序:
其实从这个例子可以看出异步的一点特点,异步就是不按照代码编写的顺序执行 感觉可以跟之前的事件模型联系在一起,先执行完在调用栈里面的,再执行异步事件(但有时候异步事件发生了也先执行异步事件,异步事件之间执行顺序也要去思考)
当p.then第一个参数中的操作出现错误时,会导致返回的Promise对象的状态发生变化。
const p = new Promise((resolve, reject) => {
resolve("成功的结果");
});
const t=p.then((value)=>{
console.log(a)//当p.then第一个参数中的操作出现错误时,会导致返回的Promise对象的状态发生变化。
console.log("成功")
},(err)=>{
console.log("失败")
}).then(value => console.log("成功2"+value),err => console.log("失败2"+err))//失败2ReferenceError: a is not defined
then返回的Promise对象状态是
const p = new Promise((resolve, reject) => {
resolve("成功的结果");
});
const t1=p.then((value)=>{
return 123//函数执行到return直接返回了 123作为返回的Promise对象的PromiseResult的值
console.log("成功")//而且因为是在第一个参数里面返回的 所以返回的Promise对象状态是fulfilled
},(err)=>{
console.log("失败")
})
console.log(t1)
//如果没有直接return的话 then返回的Promise对象的状态是pending
const t2=p.then((value)=>{
return 123
console.log("成功")
},(err)=>{
console.log("失败")
}).then(value => console.log("成功2 "+value),err => console.log("失败2"+err))//成功2 123
console.log(t2)
Promise.then.catch
其实就是Promise对象为rejected的时候调用的回调函数 catch()方法返回的还是一个 Promise 对象,因此后面还可以接着调用then()方法。
截一个阮一峰里面没太看懂的 为什么还会执行SetTimeout语句呢???
Promise.finally
finally()方法用于指定不管 Promise 对象最后状态如何.
finally方法的回调函数不接受任何参数,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。
finally也是有两种情况的!
Promise.all()
Promise.all()
方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
上面代码中,Promise.all()
方法接受一个数组作为参数,p1
、p2
、p3
都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve
方法,将参数转为 Promise 实例,再进一步处理。另外,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
的回调函数。
const databasePromise = connectDatabase();
const booksPromise = databasePromise
.then(findAllBooks);
const userPromise = databasePromise
.then(getCurrentUser);
Promise.all([
booksPromise,
userPromise
])
.then(([books, user]) => pickTopRecommendations(books, user));
上面代码中,booksPromise
和userPromise
是两个异步操作,只有等到它们的结果都返回了,才会触发pickTopRecommendations
这个回调函数。
如果作为参数的 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: 报错了]
上面代码中,p1
会resolved
,p2
首先会rejected
,但是p2
有自己的catch
方法,该方法返回的是一个新的 Promise 实例,p2
指向的实际上是这个实例。该实例执行完catch
方法后,也会变成resolved
,导致Promise.all()
方法参数里面的两个实例都会resolved
,因此会调用then
方法指定的回调函数,而不会调用catch
方法指定的回调函数。
使用Promise实现ajax
new Promise((resolve, reject) => {
$.ajax({
type: "GET",
url: "data1.json",
success: (res) => {
resolve(res);
},
error: (res) => {
reject(res);
},
});
}).then((data) => {
const { id } = data;
return new Promise((resolve, reject) => {
$.ajax({
type: "GET",
url: "data2.json",
success: (res) => {
resolve(res);
},
error: (res) => {
reject(res);
},
});
});
});
使用Promise跟Generator执行异步操作
补充:js轮询模型
如图所示 要等到调用栈里面的执行完才从微任务队列、消息队列里面拿任务出来做。
SetTimeout是在调用栈里面执行的!等到ns后,再将callback推进消息队列中,等待轮询。
注意,new Promise是构造函数,对构造函数的调用是在调用栈中执行的,Promise.then才是异步事件,放入微任务队列中。
消息队列里面的任务是宏任务,消耗的资源比微任务队列里面的微任务要多。微任务会比宏任务先执行。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mjnQZEbl-1647068414285)(C:/Users/neversleep/AppData/Roaming/Typora/typora-user-images/image-20211015154953631.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rNAy7g88-1647068414286)(C:/Users/neversleep/AppData/Roaming/Typora/typora-user-images/image-20211015160515909.png)]
补充2 协程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EB8vb63e-1647068414287)(C:/Users/neversleep/AppData/Roaming/Typora/typora-user-images/image-20211015161340108.png)]
async函数
async
就是Generator
的语法糖,用async换* await换掉yield
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SaAsvzXV-1647068414289)(C:/Users/neversleep/AppData/Roaming/Typora/typora-user-images/image-20211015161605563.png)]
async
函数的改进:
- 内置执行器,不用跟
Generator
函数一样一直next()
才能跑完了。 yield
命令后面只能是 Thunk 函数或 Promise 对象,而async
函数的await
命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)async
函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then
方法指定下一步的操作。
async
函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await
命令就是内部then
命令的语法糖。
基本用法
async
函数返回一个 Promise 对象(async
函数不管有没有return
语句,总是返回一个 Promise 对象),可以使用then
方法添加回调函数。当函数执行的时候,一旦遇到await
就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
我觉得可以这么理解:
在async
函数内部是异步当同步做的,如果有一个异步操作,先执行那个异步操作,但是在异步操作执行的时候,函数先返回了,接着执行函数后面的代码。就是是双线并行的,然后等异步好了,再执行函数里面异步后面的代码。
function timeout(ms) {
console.log("进入异步操作")
//settimeout2
setTimeout(()=>{
console.log("返回前的异步代码")
},500)
console.log("异步后")
//settimeout1
return new Promise((resolve) => {
console.log("返回函数,退出异步操作")
setTimeout(resolve, ms);
});
}
async function asyncPrint(ms) {
await timeout(ms);
console.log('异步完成');
}
function test(){
let result=asyncPrint(10)
console.log(result);
console.log("在异步后面");
//settimeout3
setTimeout(()=>{
console.log(result)
},300)
}
test()
// 进入异步操作
// 异步后
// 返回函数,退出异步操作
// Promise { <pending> }
// 在异步后面
// 异步完成
// Promise { undefined }
// 返回前的异步代码
这个例子是这样的,首先运行到函数asyncPrint
,然后进入异步操作。
-
进入函数
asyncPrint
-
进入异步函数
timeout
,走里面的同步代码 如果遇到异步代码跳过 直接冲向返回,返回一个状态为<pending>
的Promise对象(因为异步操作没做完嘛) -
现在回到函数
test
,执行函数asyncPrint
后面的打印语句 -
然后看谁做好了 谁做好了就执行谁 我感觉setTimout123,这三个任务之间没有什么先后顺序关系,就是谁好了谁打印
-
但是setTimeout1好了,
await
返回的那个Promise
的值状态会变成fullfilled
我感觉这里就跟宏任务微任务有关了,就是调用栈里面先执行能执行的同步代码,执行完了去轮询,看看消息队列(宏任务)跟微任务有没有已经搞好的,有就放到调用栈里面,谁好谁上。
由于async
函数返回的是 Promise 对象,可以作为await
命令的参数。所以,上面的例子也可以写成下面的形式。
async function timeout(ms) {
await new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
}
asyncPrint('hello world', 50);
Promise 对象的状态变化
async
函数返回的 Promise 对象,必须等到内部所有await
命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return
语句或者抛出错误。也就是说,只有async
函数内部的异步操作执行完,才会执行then
方法指定的回调函数。
sync function getTitle(url) {
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"
上面代码中,函数getTitle
内部有三个操作:抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成,才会执行then
方法里面的console.log
。
async 函数可以保留运行堆栈。
const a = () => {
b().then(() => c());
};
上面代码中,函数a
内部运行了一个异步任务b()
。当b()
运行的时候,函数a()
不会中断,而是继续执行。等到b()
运行结束,可能a()
早就运行结束了,b()
所在的上下文环境已经消失了。如果b()
或c()
报错,错误堆栈将不包括a()
。
现在将这个例子改成async
函数。
const a = async () => {
await b();
c();
};
上面代码中,b()
运行的时候,a()
是暂停执行,上下文环境都保存着。一旦b()
或c()
报错,错误堆栈将包括a()
。
async 函数的实现原理
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
await
只在异步函数(async
)里面起作用,可以放在任何异步的,基于 promise 的函数之前。await 操作符后的表达式的值不是一个 Promise,当成是resovled
的Promise,然后返回该值本身。
await 表达式会暂停当前 async function
的执行,等待 Promise 处理完成。若 Promise 正常处理(fulfilled),其回调的resolve函数参数(就是返回的值)作为 await 表达式的值,继续执行 async function
.若 Promise 处理异常(rejected),await 表达式会把 Promise 的异常原因抛出。
await
命令后面是一个thenable
对象(即定义了then
方法的对象),那么await
会将其等同于 Promise 对象。
class Sleep {
constructor(timeout) {
this.timeout = timeout;
}
then(resolve, reject) {
const startTime = Date.now();
setTimeout(
() => resolve(Date.now() - startTime),
this.timeout
);
}
}
(async () => {
const sleepTime = await new Sleep(1000);
console.log(sleepTime);
})();
// 1000
上面代码中,await
命令后面是一个Sleep
对象的实例。这个实例不是 Promise 对象,但是因为定义了then
方法,await
会将其视为Promise
处理。
任何一个await
语句后面的 Promise 对象变为reject
状态,那么整个async
函数都会中断执行。
async function f() {
await Promise.reject('出错了');
await Promise.resolve('hello world'); // 不会执行
}
上面代码中,第二个await
语句是不会执行的,因为第一个await
语句状态变成了reject
。
另一种方法是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
继发与并发
多个await
命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
let foo = await getFoo();
let bar = await getBar();
上面代码中,getFoo
和getBar
是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo
完成以后,才会执行getBar
,完全可以让它们同时触发。
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
getFoo
和getBar
都是同时触发,这样就会缩短程序的执行时间
如果将forEach
方法的参数改成async
函数,也有问题。
function dbFuc(db) { //这里不需要 async
let docs = [{}, {}, {}];
// 可能得到错误结果
docs.forEach(async function (doc) {
await db.post(doc);
});
}
上面代码可能不会正常工作,原因是这时三个db.post()
操作将是并发执行,也就是同时执行,而不是继发执行。(有点感觉,但是说不出1234)
async function logInOrder(urls) {
// 并发读取远程URL
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});
// 按次序输出
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
上面代码中,虽然map
方法的参数是async
函数,但map
函数是并发执行的,因为只有async
函数内部是继发执行,外部不受影响。后面的for..of
循环内部使用了await
,因此实现了按顺序输出。
总结:
async
告诉程序这是一个异步,await 会暂停执行async中的代码,等待await 表达式后面的结果,跳过async
函数,继续执行后面代码。
async 函数会返回一个Promise 对象,那么当 async 函数返回一个值时,Promise 的 resolve 方法会负责传递这个值;当 async 函数抛出异常时,Promise 的 reject 方法也会传递这个异常值
await 操作符用于等待一个Promise 对象,并且返回 Promise 对象的处理结果(成功把resolve 函数参数作为await 表达式的值),如果等待的不是 Promise 对象,则用 Promise.resolve(xx) 转化
补充:
js异步
const fs=require('fs');
fs.readFile('./test.txt',(err,data)=>{
console.log(data.toString())
})
Promise解决异步回调地狱
const fs = require("fs");
const { resolve } = require("path");
function readFile(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, (err, data) => {//这个是node.js约定的 错误会传入第一个参数 没有错误就是(null,data)
if (err) {
reject(err);
return;
}
resolve(data);
});
});
}
readFile('test.txt').then((data)=>{
console.log(data.toString())//说Promise解决回调地狱是说Promise把嵌套回调改成链式了
}).catch(e=>{
console.log(e)
})
async函数跟Generator相比少了next启动器 它是自己动的(内置了执行器) 不需要next再做驱动器,但是一般写的时候都写成立即执行函数比较好
使用Promise.then.catch捕捉错误的话,错误后面就不会执行,但是用try…catch捕捉的话,错误后面的代码就会执行
const fs=require('fs')
const promisify=require('util').promisify
const readFile=promisify(fs.readFile)
async function rf(){
console.log("before")
const data=await readFile('test.txt');//在这里停下来 等待异步操作的完成 再往下走
// 如果await后面是其他常规的数据类型 这里就变成同步操作
console.log(data.toString())
const data1=await readFile('test.txt')
console.log(data1.toString())
console.log("after")
}
//立即执行函数
(async (){
console.log("before")
const data=await readFile('test.txt');//在这里停下来 等待异步操作的完成 再往下走
// 如果await后面是其他常规的数据类型 这里就变成同步操作
console.log(data.toString())
const data1=await readFile('test.txt')
console.log(data1.toString())
console.log("after")
})()
rf()
//before
//hello world
//hello world
//after
async函数返回的是Promise对象所以可以使用catch对其错误进行捕抓,也可以使用try…catch进行捕抓
const fs=require('fs')
const promisify=require('util').promisify
const readFile=promisify(fs.readFile)
async function rf(){
console.log("before")
const data=await readFile('test1.txt');
console.log("after")
}
rf().catch((e)=>{
console.log(111)
})
//before
//111
const fs=require('fs')
const promisify=require('util').promisify
const readFile=promisify(fs.readFile)
async function rf(){
console.log("before")
try{
const data=await readFile('test1.txt');
}catch(e){
console.log(222)
}
console.log("after")
}
rf().catch((e)=>{
console.log(111)
})
//before
//222
//after
Set(集合)
类似数组,但是集合会自动去重。可以利用集合去重的特性,来给数组、字符串去重。
自动去重:
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach((x) => s.add(x));//别把forEach打错了!!驼峰命名法时从第二个单词开始大写!
for (let i of s) {
console.log(i);//2,3,5,4
}
利用去重特性给数组去重:
arr = [...new Set([2, 3, 5, 4, 5, 2, 2])];
console.log(arr);//[2, 3, 5, 4]
集合接受带有迭代器的数据结构来初始化,literally只要带有迭代器接口的数据结构就行
数组、选择器返回的类数组、字符串就行
// 例一
const set = new Set([1, 2, 3, 4, 4]);//数组check
[...set]
// [1, 2, 3, 4]
// 例二
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5
// 例三
const set = new Set(document.querySelectorAll('div'));//选择器返回的类数组check
set.size // 56
// 类似于
const set = new Set();
document
.querySelectorAll('div')
.forEach(div => set.add(div));
set.size // 56
//例四
//可以使用字符串构造集合,顺便去了个重
strSet=new Set('ababbc');
console.log(strSet)//Set(3) {size: 3, a, b, c}
集合的去重判断类似全等,不进行类型转换,但是NaN在集合中的去重判断中是等于它自己的,然后两个空对象是不一样的。
Array.from 集合=>数组
输出了一下 就是value方法 嘻嘻
Set的键值对的键跟值都是一样的 都是Set的value ,数组的键是下标,值是数组的值。
这波叫迂回战术。
补充:
Array.map() 方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。
map() 方法按照原始数组元素顺序依次处理元素。
let numbers = [4, 9, 16, 25];
let arr=numbers.map(Math.sqrt)//就是按照顺序一个一个开方,再放到新数组里面吧
console.log(arr)//[2, 3, 4, 5]
Array.filter(callback)
根据传的callback()函数,将数组里面的元素按照顺序一个一个传进去,返回值为true的就放进新数组里面,false就不放进新数组。
let ages=[32, 33, 16, 40];
console.log(ages.filter((age)=>{return age>18}))//[32, 33, 40]
Map
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构)。
Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。
const m=new Map()
const o={p:'hello world'}
//用set设置对象=》字符串这样 Map打破了js的键值对的键只能是字符串 改成了值=>值
//set方法返回的是当前的Map对象,因此可以采用链式写法。
m.set(o,'content')
console.log(m)//Map(1) {size: 1, {p: 'hell…rld'} => content}
//使用get对Map进行取值
console.log(m.get(o))//content
//查Map对象里面有没有o这个键 返回布尔值 有就返回true 没有也返回false
console.log(m.has(o))//true
//返回布尔值 可以传键 也可以不传 传就删减键 不传就全删
console.log(m.delete(o))//true
console.log(m.has(o))//false
双重数组初始化Map 也是掰掉两层框
const map=new Map()
map
.set(1,'aaa')
.set(1,'bbb')
console.log(map.get(1))//'bbb'
只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。
const map = new Map();
map.set(['a'], 555);
map.get(['a']) // undefined
看起来值是一样的,但是内存不一样!!!所以最好用变量存一下对象的地址,再用操作这个变量。
Map的遍历
const map = new Map([
['F', 'no'],//键 值
['T', 'yes'],
]);
for (let key of map.keys()) {
console.log(key);
}
// "F"
// "T"
for (let value of map.values()) {
console.log(value);
}
// "no"
// "yes"
//entries返回的是键值对
for (let item of map.entries()) {
console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"
// 或者
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
// 等同于使用map.entries()
for (let [key, value] of map) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
上面代码最后的那个例子,表示 Map 结构的默认遍历器接口(Symbol.iterator属性),就是entries方法。
截一块阮一峰教程里面没太看懂的部分:
怎么就实现绑定了。。
这里也不明白
class
类是构造函数的语法糖 就是完全不改变语言 但是写得更加好看了(虽然是语法糖,但是函数声明可以提升,但是类声明不能提升,而且函数受函数作用域限制,类受块作用域限制
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
其实还是通过实例化function
实例化对象。上面代码定义了一个“类”,可以看到里面有一个constructor()
方法,这就是构造方法,而this
关键字则代表实例对象。
Point
类除了构造方法,还定义了一个toString()
方法。注意,定义toString()
方法的时候,前面不需要加上function
这个关键字,直接把函数定义放进去了就可以了。方法与方法之间不需要逗号分隔,加了会报错。
class Point {
constructor() {
// ...
}
toString() {
// ...
}
toValue() {
// ...
}
}
// 等同于
Point.prototype = {
constructor() {},
toString() {},
toValue() {},
};
上面代码中,constructor()
、toString()
、toValue()
这三个方法,其实都是定义在Point.prototype
上面。
事实上,类的所有方法都定义在类的prototype
属性上面。定义在prototype
属性上才能别所有的实例访问到。还是跟之前的function.prototype
一样,所有人共享一个原型对象,如果原型对象进行修改,那么所有的实例对象都要发生改变。
class Point {
constructor() {
// ...
}
toString() {
// ...
}
toValue() {
// ...
}
}
// 等同于
Point.prototype = {
constructor() {},
toString() {},
toValue() {},
};
上面代码中,constructor()
、toString()
、toValue()
这三个方法,其实都是定义在Point.prototype
上面。
类的内部所有定义的方法,都是不可枚举的(non-enumerable)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DuWueH99-1647068414294)(D:/Typora/image-20211016114152607.png)]是说方法不可以枚举,不是说类不能遍历,可以通过给类[Symbol.iterator],加遍历接口来进行类的遍历的!
constructor()
constructor()
方法是类的默认方法,通过new
命令生成对象实例时,自动调用该方法。一个类必须有constructor()
方法,如果没有显式定义,一个空的constructor()
方法会被默认添加。
(js高程)类本身在使用new调用的时候就会被当成构造函数。类中定义的
constructor
方法不会被当成构造函数,在对它使用instanceof
操作符时会返回false
constructor()
方法默认返回实例对象(即this
),完全可以指定返回另外一个对象。
class Foo {
constructor() {
return Object.create(null);
}
}
new Foo() instanceof Foo
// false
上面代码中,constructor()
函数返回一个全新的对象,结果导致实例对象不是Foo
类的实例。
补充一点构造函数:
构造函数指的是用new调用的 给this赋值的函数 不用严格模式直接调用不会报错,用就会报错(严格模式下,this指向undefined
没有给this属性赋值的用严格模式也没有报错
function Circle() {
"use strict";
console.log("run con");
}
Circle();//run con
类的写法
类必须使用new关键字进行调用
类之间方法不能写逗号 写逗号会报错
表达式写法要注意 那个变量才能随便用 类名只能在类内部用 在外部用会报错的
类不存在提升
类里面的constructor是必须要有的 不声明会有一个自动弄一个空的 跟C++一样
class Circle {
constructor() {
console.log("run con");
}
}
circle=new Circle()//run con
Circle()//Uncaught TypeError: Class constructor Circle cannot be invoked without 'new'
类是构造函数的语法糖 没有改变语言的结构 只是写得更好看了 类还是构造函数,还是用的prototype原型链那一套,底层并没有改变 在类中定义的方法定义在prototype里面
class Circle {
constructor() {
console.log("run con");
}
}
console.log(typeof Circle)//function
console.log(Circle==Circle.prototype.constructor)//true
![image-20211015224333435](https://gitee.com/inotfine/typora-pics/raw/master/imgs//image-20211015224333435.png)
确实 es5里面只能操作显式的prototype不能操作隐式的__proto__有点依赖了 浏览器给的有点依赖了(。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Md2smX8V-1647068414296)(D:/Typora/image-20211015224628388.png)]
取值函数(getter)和存值函数(setter)
与 ES5 一样,在“类”的内部可以使用get
和set
关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
class MyClass {
constructor() {
// ...
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}
let inst = new MyClass();
inst.prop = 123;
// setter: 123
inst.prop
// 'getter'
上面代码中,prop
属性有对应的存值函数和取值函数,因此赋值和读取行为都被自定义了。
补充属性描述符:
属性描述符(descriptor)是对JavaScript属性的描述,包括:value、writable、enumerable、configurable,除value其他默认为true。
类的表达式写法
与函数一样,类也可以使用表达式的形式定义。
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是Me
,但是Me
只在 Class 的内部可用,指代当前类(如果在外部用的话会显示undefined
)。在 Class 外部,这个类只能用MyClass
引用,然而内部也能够使用MyClass
const MyClass = class Me {
constructor(){
}
setClassName(name){
this.name=name
}
getClassName() {
return MyClass.name;//表达式在里面也能用的!
}
};
let c=new MyClass()
c.setClassName(22)
console.log(c.getClassName())//Me ClassName就是类名
let c=new Me()//ReferenceError: Me is not defined
采用 Class 表达式,可以写出立即执行的 Class。
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}('张三');
person.sayName(); // "张三"
嘛上面代码中,person
是一个立即执行的类的实例(毕竟本质是函数)
类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就是undefined
)
解决方法
在构造方法中绑定this
,这样就不会找不到print
方法了。
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}
// ...
}
还有一个箭头函数的没看懂 就不放了
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
循环会自动调用这个遍历器。
类的静态成员(静态方法、静态属性)
静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static
关键字,就表示该方法不能通过实例调用,而是直接通过类来调用,这就称为“静态方法”。
跟其他c++的不一样 类的静态方法是不可以被实例对象调用的。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
如果静态方法包含this
关键字,这个this
指的是类,而不是实例。
class Foo {
static bar() {
this.baz();
}
static baz() {
console.log('hello');
}
baz() {
console.log('world');
}
}
Foo.bar() // hello
Foo.baz() // hello
上面代码中,静态方法bar
调用了this.baz
,这里的this
指的是Foo
类,而不是Foo
的实例,等同于调用Foo.baz
。另外,从这个例子还可以看出,静态方法可以与非静态方法重名。静态方法通过类来调用,非静态方法通过实例来调用。
父类的静态方法可以被子类继承。
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
}
Bar.classMethod() // 'hello'
上面代码中,父类Foo
有一个静态方法,子类Bar
可以调用这个方法。
静态方法也是可以从super
对象上调用的。(这个super对象应该类似于java的,在子类中调用父类)
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', too';
}
}
Bar.classMethod() // "hello, too"
静态属性
静态属性指的是 Class 本身的属性,即Class.propName
,而不是定义在实例对象(this
)上的属性。
class Foo {
}
Foo.prop = 1;
Foo.prop // 1
上面的写法为Foo
类定义了一个静态属性prop
。
class MyClass {
static myStaticProp = 42;
constructor() {
console.log(MyClass.myStaticProp); // 42
}
}
console.log(new MyClass())
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MOJWK9pd-1647068414298)(D:/Typora/image-20220304135049667.png)]
静态属性到底在哪里呢、、我感觉应该不是prototype
吧这么讲不太对
实例属性的新写法
实例属性除了定义在constructor()
方法里面的this
上面,也可以定义在类的最顶层。这个属性也可以定义在类的最顶层,其他都不变。
class IncreasingCounter {
_count = 0;//实例的身上的属性
//等同于
constructor() {
this._count = 0;
}
get value() {
console.log('Getting the current value!');
return this._count;
}
increment() {
this._count++;
}
}
上面代码中,实例属性_count
与取值函数value()
和increment()
方法,处于同一个层级。这时,不需要在实例属性前面加上this
。
私有方法和私有属性
用#
表示私有,跟其他语言的private
差不多
私有属性
class Point {
#x;
constructor(x = 0) {
this.#x = +x;
}
get x() {
return this.#x;
}
set x(value) {
this.#x = +value;
}
}
上面代码中,#x
就是私有属性,在Point
类之外是读取不到这个属性的。由于井号#
是属性名的一部分,使用时必须带有#
一起使用,所以#x
和x
是两个不同的属性。
私有属性不限于从this
引用,只要是在类的内部,实例也可以引用私有属性。
class Foo {
#privateValue = 42;
static getPrivateValue(foo) {
return foo.#privateValue;
}
}
Foo.getPrivateValue(new Foo()); // 42
上面代码允许从实例foo
上面引用私有属性。
私有方法
私有方法。
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()
就是一个私有方法,如果在外面调用会报错(Private field '#sum' must be declared in an enclosing class
)
#
叠static
静态私有属性、方法
私有属性和私有方法前面,也可以加上static
关键字,表示这是一个静态的私有属性或私有方法。
class FakeMath {
static PI = 22 / 7;
static #totallyRandomNumber = 4;
static #computeRandomNumber() {
return FakeMath.#totallyRandomNumber;
}
static random() {
console.log('I heard you like random numbers…')
console.log(this)//[class FakeMath] { PI: 3.142857142857143 }
return FakeMath.#computeRandomNumber();
}
}
FakeMath.PI // 3.142857142857143
FakeMath.random()
// I heard you like random numbers…
//[class FakeMath] { PI: 3.142857142857143 }
FakeMath.#totallyRandomNumber // 报错
FakeMath.#computeRandomNumber() // 报错
上面代码中,#totallyRandomNumber
是私有属性,#computeRandomNumber()
是私有方法,只能在FakeMath
这个类的内部调用,外部调用就会报错。在内部调用的时候要用写类名/this
,静态函数的this
就是指向类本身。
in
class A {
use(obj) {
if (#foo in obj) {
// 私有属性 #foo 存在
} else {
// 私有属性 #foo 不存在
}
}
}
上面示例中,in
运算符判断当前类A
的实例,是否有私有属性#foo
,如果有返回true
,否则返回false
。
in
也可以跟this
一起配合使用。
class A {
#foo = 0;
m() {
console.log(#foo in this); // true
console.log(#bar in this); // false
}
}
判断私有属性时,in
只能用在定义该私有属性的类的内部。
class A {
#foo = 0;
static test(obj) {
console.log(#foo in obj);
}
}
A.test(new A()) // true
A.test({}) // false
class B {
#foo = 0;
}
A.test(new B()) // false
上面示例中,类A
的私有属性#foo
,只能在类A
内部使用in
运算符判断,而且只对A
的实例返回true
,对于其他对象都返回false
。
子类从父类继承的私有属性,也可以使用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
运算符无效。
静态块
ES2022 引入了静态块(static block),允许在类的内部设置一个代码块,在类生成时运行一次,主要作用是对静态属性进行初始化。
class C {
static x = ...;
static y;
static z;
static {
try {
const obj = doSomethingWith(this.x);
this.y = obj.y;
this.z = obj.z;
}
catch {
this.y = ...;
this.z = ...;
}
}
}
上面代码中,类的内部有一个 static 代码块,这就是静态块。它的好处是将静态属性y
和z
的初始化逻辑,写入了类的内部,而且只运行一次。
每个类只能有一个静态块,在静态属性声明后运行。静态块的内部不能有return
语句。
静态块内部可以使用类名或this
,指代当前类。
class C {
static x = 1;
static {
this.x; // 1
// 或者
C.x; // 1
}
}
上面示例中,this.x
和C.x
都能获取静态属性x
。
静态属性的初始化,静态块还有一个作用,就是将私有属性与类的外部代码分享。
let getX;
export class C {
#x = 1;
static {
getX = obj => obj.#x;
}
}
console.log(getX(new C())); // 1
上面示例中,#x
是类的私有属性,如果类外部的getX()
方法希望获取这个属性,以前是要写在类的constructor()
方法里面,这样的话,每次新建实例都会定义一次getX()
方法。现在可以写在静态块里面,这样的话,只在类生成时定义一次。
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, "张三"); // Uncaught Error: 必须使用 new 命令生成实例
let p=Person()//Uncaught Error: 必须使用 new 命令生成实例
也可以用它实现接口
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); // 正确
子类继承父类时,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
会报错。
类的继承extends
extends
不止能继承类,也能继承普通的构造函数。
es6规定,子类一定要在构造函数construnctor()
里面调用父类的构造函数super()
!这个是因为子类的this
对象是用的父类的构造函数做出一个父类的实例,再在这个父类的实例上添加自己的属性跟方法。子类构造函数的this
就是一个指向父类实例的指针,然后运行子类构造函数的其他语句,添加子类自己的方法和属性。所以,不在子类的构造函数调用父类的构造函数是得不到子类构造函数的this
的!
为什么子类的构造函数,一定要调用super()
?原因就在于 ES6 的继承机制,与 ES5 完全不同。ES5 的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即“实例在前,继承在后”。**ES6 的继承机制,则是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”。**这就是为什么 ES6 的继承必须先调用super()
方法,因为这一步会生成一个继承父类的this
对象,没有这一步就无法继承父类。
在子类的构造函数中,只有调用super()
之后,才可以使用this
关键字,否则会报错。这是因为子类实例的构建,必须先完成父类的继承,只有super()
方法才能让子类实例继承父类。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
this.color = color; // ReferenceError
super(x, y);
this.color = color; // 正确
}
}
上面代码中,子类的constructor()
方法没有调用super()
之前,就使用this
关键字,结果报错,而放在super()
之后就是正确的。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {}
}
let cp=new ColorPoint(1,2,3)
console.log(cp)
//Must call super constructor in derived class before accessing 'this' or returning from derived constructor
就算不在子类的构造函数中使用this
,也依然会报错。要获取子类的实例对象,一定要先让父类实例化,然后再在父类上面添加属性、方法变成子类实例。
class Point{
constructor(x,y){
this.x=x;
this.y=y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + " " + super.toString(); // 调用父类的toString()
}
}
console.log(new ColorPoint(1,2,'black'))
输出图片得到:
可以看出来子类ColorPoint直接增加了继承的父类Point里面的属性调用super()不是为了初始化父类,是将父类的属性跟方法加到子类里面。子类的Prototype不是父类,而是子类里面的那个constructor
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y);
this.color = color; // 正确
}
}
let cp=new ColorPoint(1,2,3)
console.log(ColorPoint.prototype)//Point(){}
console.log(typeof ColorPoint.prototype)//object
子类构造函数的prototype
指向的原型对象是父类的一个对象。这就说明,子类实例是在父类实例上改造成为的子类实例。
如果子类没有定义constructor()
方法,这个方法会默认添加,并且里面会调用super()
。也就是说,不管有没有显式定义,任何一个子类都有constructor()
方法。
class ColorPoint extends Point {
}
// 等同于
class ColorPoint extends Point {
constructor(...args) {
super(...args);
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tNhumNBG-1647068414302)(D:/Typora/image-20211018153144768.png)]
类的继承可以继承类的静态方法
类的静态方法可以写多个 都能够被继承
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
static smile() {
console.log("smile");
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + " " + super.toString(); // 调用父类的toString()
}
}
ColorPoint.smile();//smile
console.log(ColorPoint.smile());//undefined
子类可以重写父类的属性
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
static smile() {
console.log("smile");
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
static smile(){
console.log('子类的smile')
}
}
ColorPoint.smile();//子类的smile
跟其他语言一样,子类能够继承父类的私有属性,只是看不见不能够使用。私有属性只能在定义的那个类里面使用。如果子类要使用父类私有的属性跟方法的话要可以使用父类的提供的对私有属性方法操作的接口(比如下面那个getm()
)
class Foo {
#p = 1;
#m() {
console.log('hello');
}
getm(){
this.#m()
}
}
class Bar extends Foo {
constructor() {
super();
}
}
let b=new Bar()
b.getm()//hello
console.log(b)
Object.getPrototypeOf()
Object.getPrototypeOf()
方法可以用来从子类上获取父类。
class Point { /*...*/ }
class ColorPoint extends Point { /*...*/ }
Object.getPrototypeOf(ColorPoint) === Point
// true
因此,可以使用这个方法判断,一个类是否继承了另一个类。
super
补充getter,setter
设置一个伪属性,然后设置这个伪属性怎么取值/怎么设置值,没有明写这个属性,但是这个属性可以被当做一个真的属性来用。
let a={
set:(m)=>{
this.m=m
},
get:()=>{
return this.m
}
}
a.m='2'
console.log(a.m)//2
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MeTywg0q-1647068414304)(D:/Typora/image-20211018163839160.png)]
super
这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。
super
作为函数
第一种情况,super
作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super
函数。
class Foo {
constructor(){
console.log(this)
}
#p = 1;
#m() {
console.log('hello');
}
getm(){
this.#m()
}
}
class Bar extends Foo {
constructor() {
super();
}
}
let b=new Bar()
//Bar{}
上面代码中,子类B
的构造函数之中的super()
,代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错。
注意,super
虽然代表了父类A
的构造函数,但是返回的是子类B
的实例,即super
内部的this
指的是B
的实例,因此super()
在这里相当于A.prototype.constructor.call(this)
。
super
作为对象
作为函数时,super()
只能用在子类的构造函数之中,用在其他地方就会报错。
class A {}
class B extends A {
m() {
super(); // 报错
}
}
上面代码中,super()
用在B
类的m
方法之中,就会造成语法错误。
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
调用的。 就是定义在constructor
里面,为实例添加的属性跟方法不能够通过super
对象调用,但是定义在prototype
跟父类上的行。
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
就引用不到它。
class A {
static st="静态"
hah="属性"//其实是实例属性的另外一种写法!!不要被迷惑了!
constructor() {
this.p = 2;
}
aa(){
console.log("父类函数")
}
}
class B extends A {
get m() {
console.log(super.aa)
console.log(super.hah)
console.log(super.p)
console.log(super.st)
}
}
let b = new B();
b.m
//[Function: aa] 函数行
// undefined 实例属性不行
// undefined 实例属性不行
// undefined 静态属性不行
ES6 规定,在子类普通方法中通过super
(对象)调用父类的方法时(在调用super
作为对象的时候),方法内部的**this
指向当前的子类实例**。
在子类普通方法中通过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
由于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;//constructor里面的x是B的实例对象的属性,不是B的
}
static m() {
super.print();
}
}
B.x = 3;
B.m(); // 3
使用super
的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。
class A {}
class B extends A {
constructor() {
super();
console.log(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
的实例。
类的 prototype 属性和__proto__属性
大多数浏览器的 ES5 实现之中,每一个对象都有__proto__
属性,指向对应的构造函数的prototype
属性。Class 作为构造函数的语法糖,同时有prototype
属性和__proto__
属性,因此同时存在两条继承链。
(1)子类的__proto__
属性,表示构造函数的继承,总是指向父类。
(2)子类prototype
属性的__proto__
属性,表示方法的继承,总是指向父类的prototype
属性。
class A {}
class B extends A {}
console.log(B.prototype)
console.log(B.prototype===B.super)//false
//B.prototype(原型对象(父类)) B.super父类的原型对象
console.log(B.__proto__ === A)//true
console.log(B.prototype.__proto__ === A.prototype )//true
打印B的prototype是A类的原型对象。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SLvbDs2s-1647068414306)(D:/Typora/image-20211018171303329.png)]
上面代码中,子类B
的__proto__
属性指向父类A
,子类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();
__proto__
表示的是对象的继承,prototype.__proto___
表示的是作为函数的继承,prototype
表明是一个函数。
由此可得,作为一个对象,子类(B
)的原型(__proto__
属性)是父类(A
);作为一个构造函数,子类(B
)的原型对象(prototype
属性)是父类的原型对象(prototype
属性)的实例。
B.prototype = Object.create(A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;
讨论两种情况
第一种,子类继承Object
类。
class A extends Object {
}
A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true
这种情况下,A
其实就是构造函数Object
的复制,A
的实例就是Object
的实例。
因为有继承所以,A.__proto__
指向原型对象(Object
类),A.prototype.__proto__
是A
作为一个函数,去思考继承情况,作为一个构造函数,子类(A
)的原型对象(prototype
属性)是父类的原型对象(prototype
属性)的实例。
第二种情况,不存在任何继承。
class A {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true
这种情况下,A
作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承Function.prototype
。但是,A
调用后返回一个空对象(即Object
实例),所以A.prototype.__proto__
指向构造函数(Object
)的prototype
属性。
补充setPrototypeOf
慎用setPrototypeOf
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B0pWgXGe-1647068414308)(D:/Typora/image-20211018172123386.png)]
原生构造函数的继承
继承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
构造函数会忽略参数。
多继承
写mix函数 将多个类合成一个类 再继承最后的总类
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);
}
}
}
模块化
我觉得可以理解成头文件
ES6 模块不是对象,而是通过export
命令显式指定输出的代码,再通过import
命令输入。
// ES6模块
import { stat, exists, readFile } from 'fs';
上面代码的实质是从fs
模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载。这也导致了没法引用 ES6 模块本身,因为它不是对象。
由于 ES6 模块是编译时加载,使得静态分析成为可能。
export
export
命令规定的是对外的接口(我的理解是export应该要导出一个别人可以用的变量),必须与模块内部的变量建立一一对应关系。
// 报错
export 1;
// 报错
var m = 1;
export m;
上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出 1,第二种写法它导出的是m的值1,而不是m这个变量。
摘抄一下别人的额回复,感觉说的挺对的。
因为外部需要import
var m = 1;
export m;
其实也就是export 1,外部想引用的话该怎么引入呢? import {1} from 'path'? 显然是不行的,而export {m}的接口就是m, 外部就可以直接import {m} from 'path'
当然你也可以直接export defalut m,外部import 这样写 import name from 'path' 不需要{},name取个规范名字即可
正确的写法是下面这样。
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};
export
语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
上面代码输出变量foo
,值为bar
,500 毫秒之后变成baz
。
import
import
命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js
)对外接口的名称相同。
如果想为输入的变量重新取一个名字,import
命令要使用as
关键字,将输入的变量重命名。
import { lastName as surname } from './profile.js';
import
命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。
import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
上面代码中,脚本加载了变量a
,对其重新赋值就会报错,因为a
是一个只读的接口。但是,如果a
是一个对象,改写a
的属性是允许的。
import {a} from './xxx.js'
a.foo = 'hello'; // 合法操作
a
的属性可以成功改写,并且其他模块也可以读到改写后的值。不过,这种写法很难查错,建议凡是输入的变量,都当作完全只读,不要轻易改变它的属性。
(这样的方法感觉有点像 const 只保证地址不变 其他都会发生改变
import
命令具有提升效果,会提升到整个模块的头部,首先执行。
foo();
import { foo } from 'my_module';
上面的代码不会报错,因为import
的执行早于foo
的调用。这种行为的本质是,import
命令是编译阶段执行的,在代码运行之前。
由于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';
}
上面三种写法都会报错,因为它们用到了表达式、变量和if
结构。在静态分析阶段,这些语法都是没法得到值的。
import
语句会执行所加载的模块,因此可以有下面的写法。
import 'lodash';
上面代码仅仅执行lodash
模块,但是不输入任何值。
如果多次重复执行同一句import
语句,那么只会执行一次,而不会执行多次。
import 'lodash';
import 'lodash';
上面代码加载了两次lodash
,但是只会执行一次。
import { foo } from 'my_module';
import { bar } from 'my_module';
// 等同于
import { foo, bar } from 'my_module';
上面代码中,虽然foo
和bar
在两个语句中加载,但是它们对应的是同一个my_module
模块。import
语句是 Singleton 模式(就是用的还是那一个呗)
模块的整体加载
// api.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
import * as circle from '@/api';
console.log(circle)
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7hzA228r-1647068414311)(D:/Typora/image-20220212210304120.png)]
(整体加载让我感觉export暴露出来的东西,有一点点像对象
模块整体加载所在的那个对象(上例是circle
),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的。
import * as circle from './circle';
// 下面两行都是不允许的
//这样子修改不就是在修改暴露出来的foo跟area吗 就是在修改只读值啊
circle.foo = 'hello';
circle.area = function () {};
export default
// export-default.js
export default function () {
console.log('foo');
}
其他模块加载该模块时,import
命令可以为该匿名函数指定任意名字。
// import-default.js
import customName from './export-default';
customName(); // 'foo'
// import-default.js
import customName from './export-default';
customName(); // 'foo'
上面代码的import
命令,可以用任意名称指向export-default.js
输出的方法,这时就不需要知道原模块输出的函数名。需要注意的是,这时import
命令后面,不使用大括号。(export default 导入时不使用大括号)
// export-default.js
export default function foo() {
console.log('foo');
}
// 或者写成
function foo() {
console.log('foo');
}
export default foo;
上面代码中,foo
函数的函数名foo
,在模块外部是无效的。加载的时候,视同匿名函数加载。
export与export default
// 第一组
export default function crc32() { // 输出
// ...
}
import crc32 from 'crc32'; // 输入
// 第二组
export function crc32() { // 输出
// ...
};
import {crc32} from 'crc32'; // 输入
上面代码的两组写法,第一组是使用export default
时,对应的import
语句不需要使用大括号;第二组是不使用export default
时,对应的import
语句需要使用大括号。
export default
命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default
命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应export default
命令。
本质上,export default
就是输出一个叫做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 a = 1;
export default a;
// 错误
export default var a = 1;
后面代码中,export default a
的含义是将变量a
的值赋给变量default
。所以,最后一种写法会报错。
看了一个网上的解释觉得还挺有道理的:
-
export default a
从含义上可以理解为export default = a
,按照这种带有=
赋值符的理解法,那么export default var a
就等效于export default = var a
的写法,这样很怪 -
export default Foo
这句话中的default
是一个变量,并且是一个赋值表达式, 所以export default Foo
可以等效于export const default = Foo
按照上面的等效写法,
export default const Foo = 1;
就等效于export const default = const Foo = 1;
,这种写法同时包含了变量 的定义 (default
和Foo
)、变量的赋值 (为default
和Foo
)和变量的导出, 这么多含义同时写在一个语句中,是一种没有意义的用法。
总结:感觉只是语法的问题
同样地,因为export default
命令的本质是将后面的值,赋给default
变量,所以可以直接将一个值写在export default
之后。
// 正确
export default 42;
// 报错
export 42;
上面代码中,后一句报错是因为没有指定对外的接口,而前一句指定对外接口为default
。
如果想在一条import
语句中,同时输入默认方法和其他接口,可以写成下面这样。_
是默认输入方法
import _, { each, forEach } from 'lodash';
export default function (obj) {
// ···
}
export function each(obj, iterator, context) {
// ···
}
export { each as forEach };
上面代码的最后一行的意思是,暴露出forEach
接口,默认指向each
接口,即forEach
和each
指向同一个方法。
export 与 import 的复合写法
如果在一个模块之中,先输入后输出同一个模块,import
语句可以与export
语句写在一起。
export { foo, bar } from 'my_module';
// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };
上面写成一行一行foo
和bar
只是走了一个过场,并没有导入当前的模块,当前模块用不了的。纯纯转发工具人罢了。
模块的接口改名和整体输出,也可以采用这种写法。
// 接口改名
export { foo as myFoo } from 'my_module';
// 整体输出
export * from 'my_module';
默认接口的写法如下。
export { default } from 'foo';
ES2020 之前,有一种import
语句,没有对应的复合写法。
import * as someIdentifier from "someModule";
ES2020补上了这个写法。
export * as ns from "mod";
// 等同于
import * as ns from "mod";
export {ns};
export<=>export default
具名接口改为默认接口的写法如下。
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 var e = 2.71828182846;
export default function(x) {
return Math.exp(x);
}
上面代码中的export *
,表示再输出circle
模块的所有属性和方法。注意,export *
命令会忽略circle
模块的default
方法。然后,上面代码又输出了自定义的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)
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 的require
方法,区别主要是前者是异步加载,后者是同步加载。
适用场合
下面是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
的返回结果,加载不同的模块。
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();
Module 的加载实现
srcipt
标签的异步加载:使用defer
跟async
属性
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
上面代码中,<script>
标签打开defer
或async
属性,脚本就会异步加载。渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。
async
:
-
async
一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染,所以会造成页面的空白。 -
如果有多个声明了async的脚本,其下载和执行也是异步的,不能确保彼此的先后顺序
-
async会在load事件之前执行,但不能确保与DOMContentLoaded的先后执行顺序
-
动态添加的标签隐含
async
属性 -
js高程
立即开下载脚本,但是不能阻止其他页面动作
defer
:
-
defer
要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行 -
如果有多个声明了defer的脚本,则会按顺序下载和执行
-
defer脚本会在
DOMContentLoaded和load
事件之前执行 -
js高程
表示脚本可以延迟到文档完全被解析和显示之后再执行
实际执行情况
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EEDhKIi0-1647068414312)(D:/Typora/83df384b7a5c4736a12ee295ad1a6a6btplv-k3u1fbpfcp-watermark.awebp)]
相同点:
- 加载文件时不阻塞页面渲染
- 对于
inline
的script
无效 - 使用这两个属性的脚本中不能调用
document.write
方法 - 有脚本的
onload
的事件回调 - 都只能外部脚本有效
加载规则
浏览器加载 ES6 模块,也使用<script>
标签,但是要加入type="module"
属性。
浏览器对于带有type="module"
的<script>
,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>
标签的defer
属性。(type="module"
的<script>
自动带一个defer属性呗)如果网页有多个<script type="module">
,它们会按照在页面出现的顺序依次执行。
<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>
<script>
标签的async属性也可以打开,这时只要加载完成,渲染引擎就会中断渲染立即执行。执行完成后,再恢复渲染。一旦使用了async
属性,<script type="module">
就不会按照在页面出现的顺序执行,而是只要该模块加载完成,就执行该模块。
<script type="module" src="./foo.js" async></script>