作用域和作用域链
什么是作用域
JavaScript中的作用域说的是变量的可访问性和可见性。也就是说整个程序中哪些部分可以访问这个变量,或者说这个变量都在哪些地方可见。
作用域的作用
- **作用域最为重要的一点是安全。**变量只能在特定的区域内才能被访问,有了作用域我们就可以避免在程序其它位置意外对某个变量做出修改。
- **作用域也会减轻命名的压力。**我们可以在不同的作用域下面定义相同的变量名。
作用域的类型
JavaScript中有三种作用域:
- 全局作用域;
- 函数作用域;
- 块级作用域;
1. 全局作用域
任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问。例如:
// 全局变量
var greeting = 'Hello World!';
function greet() {
console.log(greeting);
}
// 打印 'Hello World!'
greet();
2. 函数作用域
函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问。例如:
function greet() {
var greeting = 'Hello World!';
console.log(greeting);
}
// 打印 'Hello World!'
greet();
// 报错: Uncaught ReferenceError: greeting is not defined
console.log(greeting);
3. 块级作用域
ES6引入了let
和const
关键字,和var
关键字不同,在大括号中使用let
和const
声明的变量存在于块级作用域中。在大括号之外不能访问这些变量。看例子:
{
// 块级作用域中的变量
let greeting = 'Hello World!';
var lang = 'English';
console.log(greeting); // Prints 'Hello World!'
}
// 变量 'English'
console.log(lang);
// 报错:Uncaught ReferenceError: greeting is not defined
console.log(greeting);
上面代码中可以看出,在大括号内使用var
声明的变量lang是可以在大括号之外访问的。使用var
声明的变量不存在块级作用域中。
作用域嵌套
像JavaScript中函数可以在一个函数内部声明另一个函数一样,作用域也可以嵌套在另一个作用域中。
var name = 'Peter';
function greet() {
var greeting = 'Hello';
{
let lang = 'English';
console.log(`${lang}: ${greeting} ${name}`);
}
}
greet();
这里我们有三层作用域嵌套,首先第一层是一个块级作用域(let
声明的),被嵌套在一个函数作用域(greet
函数)中,最外层作用域是全局作用域。
词法作用域
词法作用域(也叫静态作用域)从字面意义上看是说作用域在词法化阶段(通常是编译阶段)确定而非执行阶段确定的。看例子:
let number = 42;
function printNumber() {
console.log(number);
}
function log() {
let number = 54;
printNumber();
}
// Prints 42
log();
上面代码可以看出无论printNumber()
在哪里调用console.log(number)
都会打印42
。动态作用域不同,console.log(number)
这行代码打印什么取决于函数printNumber()
在哪里调用。
如果是动态作用域,上面console.log(number)
这行代码就会打印54
。
使用词法作用域,我们看源代码就可以确定一个变量的作用范围,但如果是动态作用域,代码执行之前我们没法确定变量的作用范围。
作用域链
当在JavaScript中使用一个变量的时候,首先JavaScript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域。
如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错。
例如:
let foo = 'foo';
function bar() {
let baz = 'baz';
// 打印 'baz'
console.log(baz);
// 打印 'foo'
console.log(foo);
number = 42;
console.log(number); // 打印 42
}
bar();
当函数bar()
被调用,JavaScript引擎首先在当前作用域下寻找变量baz
,然后寻找foo变量但发现在当前作用域下找不到,然后继续在外部作用域寻找找到了它(这里是在全局作用域找到的)。
然后将42
赋值给变量number
。JavaScript引擎会在当前作用域以及外部作用域下一步步寻找number变量(没找到)。
如果是在非严格模式下,引擎会创建一个number
的全局变量并把42
赋值给它。但如果是严格模式下就会报错了。
**结论:**当使用一个变量的时候,JavaScript引擎会循着作用域链一层一层往上找该变量,直到找到该变量为止。
this指向
直接调用
直接调用,就是通过 函数名(...)
这种方式调用。这时候,函数内部的 this
指向视情况而定:
严格模式下是 undefined
示例:
function test() {
"use strict"; // 函数作用域内启动严格模式
console.log(this);
}
test(); // 直接调用,输出 undefined
非严格模式下是指向全局对象 globalThis
较新版本 JS 才有
globalThis
。如果是较旧版本的 JS,浏览器中的全局对象是windows
,NodeJs 中的全局对象是global
。
function test() {
console.log(this === globalThis);
}
test(); // 直接调用,输出 true
注意:使用class
语法也会自动进入严格模式
这里还需要注意,直接调用并不是指在全局作用域下进行调用,在任何作用域下,直接通过 函数名(...)
来对函数进行调用的方式,都称为直接调用。比如下面这个例子也是直接调用
(function() {
// 通过 IIFE 限定作用域
function test() {
"use strict"
// strict 模式下输出 false,此时 this === undefined
// 非 strict 模式下输出 true
console.log(this === globalThis);
}
test(); // 非全局作用域下的直接调用
})();
方法调用
方法调用是指通过对象来调用其方法函数,它是 对象.方法函数(...)
这样的调用形式。这种情况下,函数中的 this
指向调用该方法的对象。
const obj = {
// 第一种方式,定义对象的时候定义其方法
test() {
console.log(this === obj);
}
};
// 第二种方式,对象定义好之后为其附加一个方法(函数表达式)
obj.test2 = function() {
console.log(this === obj);
};
// 第三种方式和第二种方式原理相同
// 是对象定义好之后为其附加一个方法(函数定义)
function t() {
console.log(this === obj);
}
obj.test3 = t;
// 这也是为对象附加一个方法函数
// 但是这个函数绑定了一个不是 obj 的其它对象
obj.test4 = (function() {
console.log(this === obj);
}).bind({});
obj.test(); // true
obj.test2(); // true
obj.test3(); // true
// 受 bind() 影响,test4 中的 this 指向不是 obj
obj.test4(); // false
这里需要注意的是,后三种方式都是预定定义函数,再将其附加给 obj
对象作为其方法。再次强调,函数内部的 this
指向与定义无关,最终受调用方式的影响。
箭头函数中的 this
箭头函数没有自己的 this
绑定。箭头函数中使用的 this
,其实是直接包含它的那个函数或函数表达式中的 this
。比如
const obj = {
test() {
const arrow = () => {
// 这里的 this 是 test() 中的 this,
// 由 test() 的调用方式决定
console.log(this === obj);
};
arrow();
},
getArrow() {
return () => {
// 这里的 this 是 getArrow() 中的 this,
// 由 getArrow() 的调用方式决定
console.log(this === obj);
};
}
};
obj.test(); // true
const arrow = obj.getArrow();
arrow(); // true
示例中的两个 this
都是由箭头函数的直接外层函数(方法)决定的,而方法函数中的 this
是由其调用方式决定的。上例的调用方式都是方法调用,所以 this
都指向方法调用的对象,即 obj
。
箭头函数让大家在使用闭包的时候不需要太纠结 this
,不需要通过像 _this
这样的局部变量来临时引用 this
给闭包函数使用。来看一段 Babel 对箭头函数的转译可能能加深理解:
// ES6
const obj = {
getArrow() {
return () => {
console.log(this === obj);
};
}
}
// ES5,由 Babel 转译
var obj = {
getArrow: function getArrow() {
var _this = this;
return function () {
console.log(_this === obj);
};
}
};
另外需要注意的是,箭头函数不能用 new
调用,不能 bind()
到某个对象(虽然 bind()
方法调用没问题,但是不会产生预期效果)。不管在什么情况下使用箭头函数,它本身是没有绑定 this
的,它用的是直接外层函数(即包含它的最近的一层函数或函数表达式)绑定的 this
。
Apply()和call()函数
作用
apply和call两个函数作用都是调用函数。有两个参数,一个是作为函数上下文对象,另一个是作为函数参数传入上下文对象。
Apply和Call的区别
apply作为函数参数传入的是一个数组,而Call传入的是参数列表。本质上没有大的区别。对于什么时候该用什么方法,其实不用纠结。如果你的参数本来就存在一个数组中,那自然就用 apply,如果参数比较散乱相互之间没什么关联,就用 call。
let obj = {
name: 'yehuda'
};
function func(firstName, lastName) {
console.log(firstName + ' ' + this.name + ' ' + lastName);
}
let arr = ['A', 'B']
// apply 第二个参数传的是数组
func.apply(obj, ['A', 'B']); // A linxin B
// call 第二个参数传的是参数列表
func.call(obj,'A','C') // A yehuda C
func.call(obj,...arr) // A yehuda B
这样的话在某些场景就不需要使用call时对数组的参数使用扩展运算符展开操作,或者使用apply时解构。
apply 和 call 的用法
1、改变this的指向
let obj = {
name: 'yehuda'
};
function func() {
console.log(this.name);
}
func.apply(obj); // yehuda
func.call(obj); // yehuda
可以看到通过函数的调用,func()函数中this的指向从函数本身变成了对象上下文。是call 和apply 方法的第一个参数是作为函数上下文的对象,把 obj 作为参数传给了 func,此时函数里的 this 便指向了 obj 对象。
既然能够改变函数体中的this指向,那么就可以有很多种操作法,比如下面的
2、引用对象的方法
我们可以那一个比较有趣的例子做比喻
const steven = {
name: 'Steven',
phoneBattery: 70,
charge: function (level) {
console.log(this.name+ ' 原来的电量: ' + this.phoneBattery);
this.phoneBattery = level;
console.log(this.name+' 充电后的电量: ' + this.phoneBattery);
}
};
const becky = {
name: 'Backy',
phoneBattery: 30
};
steven.charge.call(becky,90)
有Steven和Backy两个人,他们手机电量一个70一个30 ,而Steven有一个充电宝。调用charge函数就可以给手机充电。Backy没有充电宝,但是它可以借用Steven的充电宝而不需要自己有一个。这个调用过程就是来利用apply或者call函数改变this指向从而引用对象的过程。
3、扩展:bind函数
bind()函数和call()功能和使用参数传递的方法是一样的。他是在 EcmaScript5 中扩展出来的,在低版本的 IE 中不兼容。主要的区别在于bind函数返回值是一个函数。这就说明bind 方法不会立即执行,而是返回一个改变了上下文 this 后的函数。所以函数的调用时机由程序员自己决定。
const steven = {
name: 'Steven',
phoneBattery: 70,
charge: function (level) {
console.log(this.name+ ' 原来的电量: ' + this.phoneBattery);
this.phoneBattery = level;
console.log(this.name+' 充电后的电量: ' + this.phoneBattery);
}
};
const becky = {
name: 'Backy',
phoneBattery: 30
};
let beckyCharge = steven.charge.bind(becky)
console.log(beckyCharge(90));
/*
log:
Backy 原来的电量: 30
VM146:9 Backy 充电后的电量: 90
*/
注意:在 JavaScript 严格模式下,如果 apply()
方法的第一个参数不是对象,则它将成为被调用函数的所有者(对象)。在“非严格”模式下,它成为全局对象。
闭包
什么是闭包?
函数嵌套函数,内部函数就是闭包。内部函数没有执行完成,外部函数变量不会被销毁。
function outerFun() {
let a = 10;
function innerFun() {
console.log(a);
}
return innerFun
}
let fun = outerFun()
fun() //10
闭包的特性
闭包有三个特性:
1.函数嵌套函数
2.函数内部可以引用外部的参数和变量
3.参数和变量不会被垃圾回收机制回收
闭包的定义及其优缺点
闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量
闭包的缺点就是常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。
闭包主要应用闭包场合主要是为了:设计私有的方法和变量。
一般函数执行完毕后,局部活动对象就被销毁,内存中仅仅保存全局作用域。
嵌套函数的闭包
function aaa() {
var a = 1;
return function(){
alert(a++)
};
}
var fun = aaa();
fun();// 1 执行后 a++,,然后a还在~
fun();// 2
fun = null;//a被回收!!
闭包会使变量始终保存在内存中,如果不当使用会增大内存消耗。
使用闭包的好处
那么使用闭包有什么好处呢?使用闭包的好处是:
1.希望一个变量长期驻扎在内存中
2.避免全局变量的污染
3.私有成员的存在
稀疏数组和密集数组
密集数组:占据连续的内存空间,数组元素之间紧密相连,不存在间隙。索引连续, 数组长度等于元素个数的数组;
稀疏数组:数组元素之间存在间隙。 索引不连续,数组长度大于元素个数的数组, 可以简单理解为有 empty
的数组;
如何初始化
稀疏数组
const arr = new Array(3);
密集数组
const arr = new Array.apply(null,Array(3));
const arr = new Array.from({length:3},()=>{})
稀疏数组特性
稀疏数组在大多数遍历数组的方法中,遇到「empty」元素的时候,callback 函数是不会执行的,如:map, forEach, filter
const arr = [3,,4,,5] // 稀疏数组
arr.forEach(item => { console.log(item)}) // 输出:3,4,5
console.log(arr.map(item => {
console.log(item)
return item+1
})) // 输出:3,4,5,[4, empty, 5, empty, 6]
// 值得注意的是:稀疏数组中 「empty」元素在 map 后返回的数组中仍然为 「empty」
console.log(arr.filter(item => item === undefined)) // 输出:[]
console.log(arr.filter(item => item > 3 )) // 输出:[4,5]
for (var i in arr) { console.log(arr[i]) } // 输出:3,4,5
for (var i of arr) { console.log(i) } // 输出:3,undefined,4,undefined,5
深拷贝和浅拷贝的实现
浅拷贝的实现方式
Object.assign()
Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign()进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。
var obj = {
a: {
a: "kobe",
b: 39
}
};
var initalobj = Object.assign({}, obj);
initalobj.a.a = "wade";
console.log(obj.a.a); //wade
注意:当object只有一层的时候,是深拷贝
let obj = {
username: ' kobe '
};
let obj2 = Object.assign({}, obj);
obj2.username = ' wade ';
console.log(obj); //{username: "kobe"}
Array.prototype.concat()
let arr = [1, 3, { username: ' kobe '}];
let arr2 = arr.concat();
arr2[2].username = ' wade';
// 修改新对象会改到原对象[ 1, 3, { username: ' wade' } ]
console.log(arr);
Array.prototype.slice()
let arr = [1, 3, { username: 'kobe' }];
let arr3 = arr.slice();
arr3[2].username = 'wade'
// 同样修改新对象会改到原对象 [ 1, 3, { username: ' wade' } ]
console.log(arr);
关于Array的slice和concat方法的补充说明:Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。
深拷贝的实现方式
JSON.parse(JSON.stringify())
let arr = [1, 3, {username:' kobe '}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = ' duncan ';
console.log(arr, arr4)
原理: 用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。
这种方法虽然可以实现数组或对象深拷贝,但不能处理函数
let arr = [1, 3, {username: ' kobe '}, function () { }];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = ' duncan';
console.log(arr, arr4)
这是因为JSON.stringify() 方法是将一个JavaScript值(对象或者数组)转换为一个 JSON字符串,不能接受函数
封装深拷贝方法
递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝
//实现深度克隆 ---对象/数组
function clone(target) {
//判断拷贝的数据类型
//初始化变量result成为最终克隆的数据
let result, targetType = Object.prototype.toString.call(target).slice(8, -1)
if (targetType === 'object') {
result = {}
} else if (targetType === ' Array') {
result = []
} else {
return target
}
//遍历目标数据
for (let i in target) {
//获取遍历数据结构的每项值。
let value = target[i]
//判断日标结构里的每一值 是否存在对象/数组
if (checkedType(value) === 'Object' ||
checkedType(value) === 'Array') { //对象/数组里嵌套了对象/数组
//继续遍历获取到value值
result[i] = clone(value)
} else { //获取到value值是基本的数据类型或者是函数。
result[i] = value;
}
}
return result
}
class语法糖
class是一个语法糖,其底层还是通过 构造函数
去创建的。所以它的绝大部分功能,ES5 都可以做到。新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayName = function() {
return this.name;
}
const xiaoming = new Person('小明', 18);
console.log(xiaoming);
上面代码用 ES6
的class
实现,就是下面这样
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayName() {
return this.name;
}
}
const xiaoming = new Person('小明', 18)
console.log(xiaoming);
// { name: '小明', age: 18 }
console.log((typeof Person));
// function
console.log(Person === Person.prototype.constructor);
// true
constructor方法,这就是构造方法,this关键字代表实例对象。
类的数据类型就是函数,类本身就指向构造函数。
定义类的时候,前面不需要加 function, 而且方法之间不需要逗号分隔,加了会报错。
类的所有方法都定义在类的prototype属性上面。
class A {
constructor() {}
toString() {}
toValue() {}
}
// 等同于
A.prototype = {
constructor() {},
toString() {},
toValue() {},
};
在类的实例上面调用方法,其实就是调用原型上的方法。
let a = new A();
a.constructor === A.prototype.constructor // true
constructor 方法
constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。
class A {
}
// 等同于
class A {
constructor() {}
}
constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。
class A {
constructor() {
return Object.create(null);
}
}
console.log((new A()) instanceof A);
// false
类的实例
实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。
注意:
- class不存在变量提升
new A(); // ReferenceError
class A {}
因为 ES6 不会把类的声明提升到代码头部。这种规定的原因与继承有关,必须保证子类在父类之后定义。
{
let A = class {};
class B extends A {}
}
上面的代码不会报错,因为 B继承 A的时候,A已经有了定义。但是,如果存在 class提升,上面代码就会报错,因为 class 会被提升到代码头部,而let命令是不提升的,所以导致 B 继承 A 的时候,Foo还没有定义。
- this的指向
类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。
静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。
如果在一个方法前,加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为"静态方法"。
class A {
static classMethod() {
return 'hello';
}
}
A.classMethod();
console.log(A.classMethod());
// 'hello'
const a = new A();
a.classMethod();
// TypeError: a.classMethod is not a function
A
类的classMethod
方法前有 static
关键字,表明这是一个静态方法,可以在 A
类上直接调用,而不是在实例上调用
在实例a
上调用静态方法,会抛出一个错误,表示不存在改方法。
如果静态方法包含this关键字,这个this指的是类,而不是实例。
class A {
static classMethod() {
this.baz();
}
static baz() {
console.log('hello');
}
baz() {
console.log('world');
}
}
A.classMethod();
// hello
静态方法classMethod
调用了this.baz
,这里的this
指的是A
类,而不是A
的实例,等同于调用A.baz
。另外,从这个例子还可以看出,静态方法可以与非静态方法重名。
父类的静态方法,可以被子类继承。
class A {
static classMethod() {
console.log('hello');
}
}
class B extends A {}
B.classMethod() // 'hello'
静态属性
静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。
写法是在实例属性的前面,加上static关键字。
class MyClass {
static myStaticProp = 42;
constructor() {
console.log(MyClass.myStaticProp); // 42
}
}
继承
Class 可以通过extends关键字实现继承
class Animal {}
class Cat extends Animal { };
上面代码中 定义了一个 Cat 类,该类通过 extends
关键字,继承了 Animal 类中所有的属性和方法。
但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Animal类。
下面,我们在Cat内部加上代码。
class Cat extends Animal {
constructor(name, age, color) {
// 调用父类的constructor(name, age)
super(name, age);
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。
子类必须在 constructor 方法中调用 super 方法,否则新建实例就会报错。
这是因为子类自己的this对象,必须先通过 父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。
class Animal { /* ... */ }
class Cat extends Animal {
constructor() {
}
}
let cp = new Cat();
// ReferenceError
Cat 继承了父类 Animal,但是它的构造函数没有调用super方法,导致新建实例报错。
ES5的继承,实质是先创建了子类的实例对象 this, 然后再将 父类的方法添加到 this上面。
ES6的继承机制完全不同,实质是先将 父类实例对象的属性和方法,加到 this 上面(所以必须先调用 super 方法),然后再用子类的构造函数修改 this。
如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor方法。
class Cat extends Animal {
}
// 等同于
class Cat extends Animal {
constructor(...args) {
super(...args);
}
}
另一个需要注意的地方是,在子类的构造函数中,只有调用super
之后,才可以使用this
关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super
方法才能调用父类实例。
class A {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class B extends A {
constructor(x, y, name) {
this.name = name; // ReferenceError
super(x, y);
this.name = name; // 正确
}
}
上面代码中,子类的constructor方法没有调用super之前,就使用this关键字,结果报错,而放在super方法之后就是正确的。
父类的静态方法,也会被子类继承。
class A {
static hello() {
console.log('hello world');
}
}
class B extends A {
}
B.hello() // hello world
Promise
Promise是用来干什么的?
看阮老师的ES6出门上说Promise是JS异步编程的一种解决方案. 举个例子, Ajax的回调问题, 如果下一个ajax请求要用到上一个Ajax请求中的结果, 那么往往就会导致多个回调嵌套的问题, 那么Promise就可以解决这种代码上的嵌套问题, 是我们的代码变得更优美, 更利于维护; 我暂时先对Promise的理解就是: 处理异步任务, 保存异步结果状态, 异步代码同步化…
Promise是什么?
Promise 它就是一个对象,相当于一个容器, 里面存的就是一个异步操作的结果; 我们可以是从中获取异步操作结果的相关信息。
Promise对象代表一个未完成、但预计将来会完成的操作。
它有以下三种状态:
pending:初始值,不是fulfilled,也不是rejected
fulfilled:代表操作成功
rejected:代表操作失败
Promise有两种状态改变的方式,既可以从pending转变为fulfilled,也可以从pending转变为rejected。一旦状态改变,就「凝固」了,会一直保持这个状态,不会再发生变化。当状态发生变化,promise.then绑定的函数就会被调用。
注意:Promise一旦新建就会「立即执行」,无法取消。这也是它的缺点之一。
Promise的创建和使用?
1.创建promise对象
//1.使用new Promise(func)的形式
//2.快捷语法: Promise.resolve(func) || Promise.reject(func)
// 参数1: 一般是一个处理异步任务的函数
// 返回值: 一个promise实例对象
Promise.resolve('foo')
// 等价于, 不过参数类型不一样执行的操作也会有所不同
new Promise(resolve => resolve('foo'))
2.在函数func中 放异步处理代码
// 传入两个参数: 回调函数resolve, reject分别去保存异步处理的结果
// 成功: 使用resolve(结果)
// 失败: 使用reject(原因)
3.调用实例的then(func1) 或者 catch(err)
首先then方法是异步执行, 对上面的异步结果进行处理的函数
参数: 传回调函数, 一个两个都行, 前者是成功状态的回调,后者是失败的回调
Promise常用的场景?
-
promise一般的使用套路就是:
1.先定义一个函数, 函数内部使用new Promise()的方式来返回一个promise对象, resolve用来保存 异步处理成功的结果
reject用来保存 异常处理的结果
2.然后函数调用,传参
3.链式语法点出then方法, then中的回调用来处理异步结果
4.有错误就点出catch方法, 也可以用then(null, function() {})代替catch
5.then的回调中也可return一个值, 会被包装成一个新的promise, 因此可以继续调用then方法 -
应用场景: 在ajax中使用, 解决异步嵌套问题
function ajax(url) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
// 请求类型, 地址, 异步
xhr.open('get', url, true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
try {
// 处理响应内容, 将内容丢到成功状态的回调
resolve(JSON.parse(xhr.responseText))
} catch (e) {
// 捕获错误, 丢到失败状态的回调
reject(e)
}
}
}
});
}
// 调用 封装的ajax函数
let url = 'http://127.0.0.1:3000/xxoo'; // 自己本地开的一个服务
ajax(url)
.then(res => console.log(res)) // 输出 {code: 0, msg: 'hello cors'}
.catch(err => console.log(err))
-
其他场景
// 实现串行任务管道; 即当前任务的输出可以作为下一个任务的输入,形成一条数据管道; // 比如: 比如从url1获取参数userId,拿到后再从url2获取第三方openId,最后再从url3货取orderList, // 然后把结果展示给用户,类似的逻辑都是任务管道: new Promise(function(resolve, reject) { resolve(1); }) .then(function(res) { return new Promise(function(resolve, reject) { resolve(res + 1); }); }) .then(function(res) { return new Promise(function(resolve, reject) { resolve(res + 1); }); }) .then(function(res) { console.log(res); // 3 });
promise的好处
-
在异步执行的流程中,使用Promise可以把 执行代码 和 处理结果 的代码清晰地分离
这样我们便可以 把执行代码 和 结果处理 分成不同的模块来写,易于维护
-
减少异步回调的嵌套, 比如ajax回调, 我们可以依次调用then方法即可, 还可以控制回调的顺序
-
多个异步任务是为了容错去访问用同一资源时, 可以使用Promise.race([promise实例…])
-
多个异步任务并行执行时,比如ajax访问两个接口, 可以用Promise.all([promise实例…])
Promise使用的注意事项
- Promise构造函数内的同步代码立即执行
- 回调函数参数resolve异步执行, 将结果作为参数传给then方法中的回调函数
- resolve只有第一次执行有效,状态不能二次改变
- then和catch如果有return, 返回的是一个全新的promise对象, 可以链式调用
- Promise构造函数只会执行一次, promise实例会保存resolve的状态,
以后这个实例每次调用then都是返回一个这个状态, 若链式调用then,下一个则会打印undefined, res没有值… - then中返回任意一个非 promise 的值都会被包裹成 promise 对象
- .then 或 .catch 返回的值不能是 promise 本身,否则会造成死循环
- .then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透。
- .then 可以接收两个参数,第一个是处理成功的函数,第二个是处理错误的函数。.catch 是 .then 第二个参数的简便写法,但是它们用法上有一点需要注意:.then 的第二个处理错误的函数捕获不了第一个处理成功的函数抛出的错误,而后续的 .catch 可以捕获之前的错误。
async/await 语法糖
1、async await成对出现,await再async定义的函数内
2、async定义的是函数
3、async 返回一个Promise
4、async 函数中 return 的结果将作为回调的参数
5、await后面可以是promise也可以是普通数据类型,如果是不同类型直接进 Promise 的 resolve
6、await后边一但出现reject就会终止后边的操作,直接进reject,即使这里没有return,也一样可以传入错误回调的参数
所以当一个 async 函数中有多个 await命令时,如果不想因为一个出错而导致其与的都无法执行,应将await放在try…catch语句中执行
事件循环和宏任务/微任务
事件循环与消息队列
JS是一门单线程的语言,所有的任务都是在一个线程上完成的。而我们知道,有一些像I/O,网络请求等等的操作可能会特别耗时,如果程序使用"同步模式"等到任务返回再继续执行,就会使得整个任务的执行特别缓慢,运行过程大部分事件都在等待耗时操作的完成,效率特别低。
为了解决这个问题,于是就有了**事件循环(Event Loop)**这样的概念,简单来说就是在程序本身运行的主线程会形成一个"执行栈",除此之外,设立一个"任务队列",每当有异步任务完成之后,就会在"任务队列"中放置一个事件,当"执行栈"所有的任务都完成之后,会去"任务队列"中看有没有事件,有的话就放到"执行栈"中执行。
这个过程会不断重复,这种机制就被称为事件循环(Event Loop)机制。
宏任务/微任务
宏任务可以被理解为每次"执行栈"中所执行的代码,而浏览器会在每次宏任务执行结束后,在下一个宏任务执行开始前,对页面进行渲染,而宏任务包括:
- script(整体代码)
- setTimeout
- setInterval
- I/O
- UI交互事件
- postMessage
- MessageChannel
- setImmediate
- UI rendering
微任务,可以理解是在当前"执行栈"中的任务执行结束后立即执行的任务。而且早于页面渲染和取任务队列中的任务。宏任务包括:
- Promise.then
- Object.observe
- MutaionObserver
- process.nextTick
他们的运行机制是这样的:
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
- 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
在了解了宏任务和微任务之后,整个Event Loop的流程图就可以用下面的流程图来概括: