什么是原型
原型是 JavaScript 的重要特性之一,可以让对象从其他对象继承功能特性,所以 JavaScript 也被称为“基于原型的语言”。 严格地说,原型应该是对象的特性,但函数其实也是一种特殊的对象。例如,我们对自定义的函数进行 instanceof Object 操作时,其结果是 true
function fn() {}
fn instanceof Object; // true
原型与构造函数
在 js 中我们是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性值,这个属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。
当我们使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说我们是不应该能够获取到这个值的,但是现在浏览器中都实现了 __proto__
属性来让我们访问这个属性,但是我们最好不要使用这个属性,因为它不是规范中规定的。
ES5 中新增了一个 Object.getPrototypeOf() 方法,我们可以通过这个方法来获取对象的原型。当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。
原型链的尽头一般来说都是 Object.prototype 所以这就是我们新建的对象为什么能够使用 toString()等方法的原因。
获取原型的方法
假如 p 是一个实例,p 获取原型的方法如下三种 其中 obj.constructor 指向构造函数
obj.__proto__
obj.constructor.prototype
Object.getPrototypeOf(obj)
function a() {
this.a = 1;
this.b = 2;
}
let obj = new a();
console.log(obj);
console.log(obj.constructor === a.prototype.constructor); //true
console.log(obj.constructor.prototype === a.prototype); //true
console.log(a.prototype.constructor.prototype === a.prototype); //true
console.log(a.prototype.isPrototypeOf(obj)); // true
提示 JavaScript 对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与之相关的对象也会继承这一改变。 提示 浏览器中都实现了__proto__
属性来让我们访问[[Prototype]]
的值,但是我们最好不要使用这个属性,因为它不是规范中规定的 虽然我们在脚本中没有办法访问到[[Prototype]]
属性,但是isPrototypeOf()
方法用于测试一个对象是否存在于另一个对象的原型链上。 在 ECMAScript 5 中新增了一个方法叫Object.getPrototypeOf()
,这个方法可以返回[[Prototype]]
的值
隐式原型和显式原型
隐式原型通常在创建实例的时候就会自动指向构造函数的显式原型。
例如,在下面的示例代码中,当创建对象 a 时,a 的隐式原型会指向构造函数 Object() 的显式原型
var a = {};
a.__proto__ === Object.prototype; // true
var b = new Object();
b.__proto__ === a.__proto__; // true
显式原型是内置函数(比如 Date() 函数)的默认属性,在自定义函数时(箭头函数除外)也会默认生成,生成的显式原型对象只有一个属性 constructor ,该属性指向函数自身。通常配合 new 关键字一起使用,当通过 new 关键字创建函数实例时,会将实例的隐式原型指向构造函数的显式原型
function fn() {}
fn.prototype.constructor === fn; // true
隐式原型是否必须与显式原型配合使用呢? 下面的代码声明了 parent 和 child 两个对象,其中对象 child 定义了属性 name 和隐式原型 proto,隐式原型指向对象 parent,对象 parent 定义了 code 和 name 两个属性。 当打印 child.name 的时候会输出对象 child 的 name 属性值,当打印 child.code 时由于对象 child 没有属性 code,所以会找到原型对象 parent 的属性 code,将 parent.code 的值打印出来。同时可以通过打印结果看到,对象 parent 并没有显式原型属性。如果要区分对象 child 的属性是否继承自原型对象,可以通过 hasOwnProperty() 函数来判断
var parent = { code: "p", name: "parent" };
var child = { __proto__: parent, name: "child" };
console.log(parent.prototype); // undefined
console.log(child.name); // "child"
console.log(child.code); // "p"
child.hasOwnProperty("name"); // true
child.hasOwnProperty("code"); // false
在这个例子中,如果对象 parent 也没有属性 code,那么会继续在对象 parent 的原型对象中寻找属性 code,以此类推,逐个原型对象依次进行查找,直到找到属性 code 或原型对象没有指向时停止。 这种类似递归的链式查找机制被称作“原型链”
原型的属性
属性的访问
每当代码读取对象的某个属性时,首先会在对象本身搜索这个属性,如果找到该属性就返回该属性的值,如果没有找到,则继续搜索该对象对应的原型对象,以此类推下去。
因为这样的搜索过程,因此我们如果在实例中添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性,因为在实例中搜索到该属性后就不会再向后搜索了
属性判断
既然一个属性既可能是实例本身的,也有可能是其原型对象的,那么我们该如何来判断呢?
答:使用 hasOwnProperty()
Javascript 中,有一个函数,执行时对象查找时,永远不会去查找原型
hasOwnProperty 所有继承了 Object 的对象都会继承到 hasOwnProperty 方法。这个方法可以用来检测一个对象是否含有特定的自身属性,和 in 运算符不同,该方法会忽略掉那些从原型链上继承到的属性。
在使用 for-in 循环时,返回的是所有能够通过对象访问的、可枚举的属性,其中包括了存在于实例中的属性,也包括了存在于原型中的属性。 需要注意的一点是,屏蔽了实例中不可枚举属性的实例属性也会在 for-in 循环中返回。 提示 因此我们可以封装这样一个函数,来判断一个属性是否存在于原型中
function hasPrototypeProperty(object, name) {
return !object.hasOwnProperty(name) && name in object;
}
获取所有属性
如果想要获得对象上所有可枚举的实例属性,可以使用 Object.keys() 方法,这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。 如果想要获取所有的实例属性,无论它是否可以枚举,我们可以使用 Object.getOwnPropertyNames()
方法
什么是原型链
通过一个对象的 proto 可以找到它的原型对象,原型对象也是一个对象,就可以通过原型对象的 __proto__
,最后找到了我们的 Object.prototype,从实例的原型对象开始一直到 Object.prototype 就是我们的原型链 在介绍原型时就引出了原型链的概念 其实上面将原型就已经包含了许多原型链的知识 当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就是我们新建的对象为什么能够使用 toString()等方法的原因 这种类似递归的链式查找机制被称作“原型链”
案例
在 Array 本地对象上加原型方法
用途是去重升序排序,最后返回新数组
Array.prototype.distinct = function() {
return [...new Set(this)].sort((a, b) => a - b);
};
console.log(["a", "b", "c", "d", "b", "a", "e"].distinct()); // ["a", "b"]
通过原型链实现多层继承
假设构造函数 B() 需要继承构造函数 A(),就可以通过将函数 B() 的显式原型指向一个函数 A() 的实例,然后再对 B 的显式原型进行扩展。那么通过函数 B() 创建的实例,既能访问用函数 B() 的属性 b,也能访问函数 A() 的属性 a,从而实现了多层继承。
function A() {}
A.prototype.a = function() {
return "a";
};
function B() {}
B.prototype = new A();
B.prototype.b = function() {
return "b";
};
var c = new B();
c.b(); // 'b'
c.a(); // 'a'
函数也是对象
function Foo(who) {
this.me = who;
}
Foo.prototype.identify = function() {
return "I am " + this.me;
};
function Bar(who) {
Foo.call(this, who);
}
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.speak = function() {
alert("Hello, " + this.identify() + ".");
};
var b1 = new Bar("b1");
var b2 = new Bar("b2");
b1.speak(); //Hello, I am b1
b2.speak(); //Hello, I am b2
作用域和闭包
定义
红宝书:闭包是指那些引用了另一个函数作用域中变量的函数,通常是嵌套在函数中实现的
小黄书:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行
MDN :闭包是指那些能够访问自由变量的函数(其中自由变量,指在函数中使用的,但既不是函数参数 arguments 也不是函数的局部变量的变量,其实就是另外一个函数作用域中的变量。)
所以: 闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。
闭包的用途。
- 在函数外部能够访问到函数内部的变量。通过使用闭包,我们可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
- 使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。其实闭包的本质就是作用域链的一个特殊的应用
产生原因
在 ES5 中只存在两种作用域————全局作用域和函数作用域,当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链,值得注意的是,每一个子函数都会拷贝上级的作用域,形成一个作用域的链条。
var a = 1;
function f1() {
var a = 2;
function f2() {
var a = 3;
console.log(a); //3
}
}
解析 在这段代码中,f1 的作用域指向有全局作用域(window)和它本身,而 f2 的作用域指向全局作用域(window)、f1 和它本身。而且作用域是从最底层向上找,直到找到全局作用域 window 为止,如果全局还没有的话就会报错
闭包产生的本质,就是当前环境中存在指向父级作用域的引用。
function f1() {
var a = 2;
function f2() {
console.log(a); //2
}
return f2;
}
var x = f1();
x();
解析 这里 x 会拿到父级作用域中的变量,输出 2
因为在当前环境中,含有对 f2 的引用,f2 恰恰引用了 window、f1 和 f2 的作用域。因此 f2 可以访问到 f1 的作用域的变量 这里是返回函数的情况
回到闭包的本质,我们只需要让父级作用域的引用存在即可,因此我们还可以这么做:
var f3;
function f1() {
var a = 2;
f3 = function() {
console.log(a);
};
}
f1();
f3();
解析:让 f1 执行,给 f3 赋值后,等于说现在 f3 拥有了 window、f1 和 f3 本身这几个作用域的访问权限,还是自底向上查找,最近是在 f1 中找到了 a,因此输出 2。 在这里是外面的变量 f3 存在着父级作用域的引用,因此产生了闭包,形式变了,本质没有改变
如何使用
1.返回一个函数,上面已经举例 2.作为函数参数传递
var a = 1;
function foo() {
var a = 2;
function baz() {
console.log(a);
}
bar(baz);
}
function bar(fn) {
// 这就是闭包
fn();
}
// 输出2,而不是1
foo();
**3.在定时器、事件监听、Ajax 请求、跨窗口通信、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包 以下的闭包保存的仅仅是 window 和当前作用域 **
//定时器
setTimeout(function timeHandler(){
console.log('111');
},100)
// 事件监听
$('#app').click(function(){
console.log('DOM Listener');
})
4.(立即执行函数表达式)创建闭包, 保存了全局作用域 window 和当前函数的作用域,因此可以全局的变量
var a = 2;
(function IIFE() {
// 输出2
console.log(a);
})();
优缺点
三大特性:
- 为创建内部作用域而调用了一个包装函数(函数嵌套函数)
- 包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包(函数内部可以引用外部的参数和变量)
- 参数和变量不会被垃圾回收机制回收。 优点:
- 希望一个变量长期存储在内存中。
- 避免全局变量的污染。
- 私有成员的存在。 缺点:
- 常驻内存,
- 增加内存使用量。
- 使用不当会很容易造成内存泄露。
function outer() {
var name = "jack";
function inner() {
console.log(name);
}
return inner;
}
outer()(); // jack
function sayHi(name) {
return () => {
console.log(`Hi! ${name}`);
};
}
const test = sayHi("xiaoming");
test(); // Hi! xiaoming
解析
虽然 sayHi 函数已经执行完毕,但是其活动对象也不会被销毁,因为 test 函数仍然引用着 sayHi 函数中的变量 name,这就是闭包。 但也因为闭包引用着另一个函数的变量,导致另一个函数已经不使用了也无法销毁,所以闭包使用过多,会占用较多的内存,这也是一个副作用。 由于在 ECMA2015 中,只有函数才能分割作用域,函数内部可以访问当前作用域的变量,但是外部无法访问函数内部的变量,所以闭包可以理解成“定义在一个函数内部的函数,外部可以通过内部返回的函数访问内部函数的变量“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁
案例
循环输出
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, 0);
}
//6 6 6 6 6
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
//6 6 6 6 6
因为 setTimeout 为宏任务,由于 JS 中单线程 eventLoop 机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行,但输出 i 的时候当前作用域没有,往上一级再找,发现了 i,此时循环已经结束,i 变成了 6。因此会全部输出 6
解决方法:
利用 IIFE(立即执行函数表达式)
当每次 for 循环时,把此时的 i 变量传递到定时器中
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, 0);
})(i);
}
提示 这种方法属于使用闭包解决
给定时器传入第三个参数, 作为 timer 函数的第一个函数参数
for (var i = 1; i <= 5; i++) {
setTimeout(
function timer(j) {
console.log(j);
},
0,
i
);
}
setTimeout 的参数
function : 你想要在到期时间(delay 毫秒)之后执行的函数。
code : 这是一个可选语法,你可以使用字符串而不是function,在 delay 毫秒之后编译和执行字符串 (使用该语法是不推荐的, 原因和使用 eval()一样,有安全风险)。
delay (可选) : 延迟的毫秒数 (一秒等于 1000 毫秒),函数的调用会在该延迟之后发生。如果省略该参数,delay 取默认值 0,意味着“马上”执行,或者尽快执行。不管是哪种情况,实际的延迟时间可能会比期待的(delay 毫秒数) 值长,原因请查看实际延时比设定值更久的原因:最小延迟时间。
arg1, ..., argN (可选) : 附加参数,一旦定时器到期,它们会作为参数传递给 function
使用 ES6 中的
let for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, 0);
}
关于 let let 使 JS 发生革命性的变化,让 JS 有函数作用域变为了块级作用域,用 let 后作用域链不复存在。代码的作用域以块级为单位
模拟私有变量
const book = (function() {
var page = 100;
return function() {
this.auther = "okaychen";
this._page = function() {
console.log(page);
};
};
})();
var a = new book();
a.auther; // "okaychen"
a._page(); // 100
a.page; // undefined
使用闭包打印标签的 index
<ul id="test">
<li>这是第一条</li>
<li>这是第二条</li>
<li>这是第三条</li>
</ul>
// 方法一:
var lis = document.getElementById("test").getElementsByTagName("li");
for (var i = 0; i < 3; i++) {
lis[i].index = i;
lis[i].onclick = function() {
alert(this.index);
};
}
//方法二:
var lis = document.getElementById("test").getElementsByTagName("li");
for (var i = 0; i < 3; i++) {
lis[i].index = i;
lis[i].onclick = (function(a) {
return function() {
alert(a);
};
})(i);
}
实现单例模式
var SingleStudent = (function() {
function Student() {}
var _student;
return function() {
if (_student) return _student;
_student = new Student();
return _student;
};
})();
var s = new SingleStudent();
var s2 = new SingleStudent();
s === s2; // true
闭包与模块
来自小黄书 考虑以下代码:
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join(" ! "));
}
return {
doSomething: doSomething,
doAnother: doAnother,
};
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
这个模式在 JavaScript 中被称为模块。最常见的实现模块模式的方法通常被称为模块暴露,这里展示的是其变体
首先, CoolModule() 只是一个函数,必须要通过调用它来创建一个模块实例。如果不执行外部函数,内部作用域和闭包都无法被创建。
其次, CoolModule() 返回一个用对象字面量语法 { key: value, ... } 来表示的对象。这个返回的对象中含有对内部函数而不是内部数据变量的引用。我们保持内部数据变量是隐藏且私有的状态。可以将这个对象类型的返回值看作本质上是模块的公共 API。
这个对象类型的返回值最终被赋值给外部的变量 foo ,然后就可以通过它来访问 API 中的属性方法,比如 foo.doSomething() 。
doSomething() 和 doAnother() 函数具有涵盖模块实例内部作用域的闭包(通过调用 CoolModule() 实现)
实现模块的单例模式
var foo = (function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join(" ! "));
}
return {
doSomething: doSomething,
doAnother: doAnother,
};
})();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
将模块函数转换成了 IIFE,
立即调用这个函数并将返回值直接赋值给单例的模块实例标识符 foo 命名将要作为公共 API 返回的对象 这是模块模式的另一个简单但强大的用法
var foo = (function CoolModule(id) {
function change() {
// 修改公共 API
publicAPI.identify = identify2;
}
function identify1() {
console.log(id);
}
function identify2() {
console.log(id.toUpperCase());
}
var publicAPI = {
change: change,
identify: identify1,
};
return publicAPI;
})("foo module");
foo.identify(); // foo module
foo.change();
foo.identify(); // FOO MODULE
异步
同步与异步
同步就是在执行某段代码时,在该代码没有得到返回结果之前,其他代码暂时是无法执行的,但是一旦执行完成拿到返回值之后,就可以执行其他代码了。换句话说,在此段代码执行完未返回结果之前,会阻塞之后的代码执行,这样的情况称为同步 异步就是当某一代码执行异步过程调用发出后,这段代码不会立刻得到返回结果。而是在异步调用发出之后,一般通过回调函数处理这个调用之后拿到结果。异步调用发出后,不会影响阻塞后面的代码执行,这样的情形称为异步。
why
JavaScript 是单线程的,如果 JS 都是同步代码执行意味着什么呢?这样可能会造成阻塞,如果当前我们有一段代码需要执行时,如果使用同步的方式,那么就会阻塞后面的代码执行;而如果使用异步则不会阻塞,我们不需要等待异步代码执行的返回结果,可以继续执行该异步任务之后的代码逻辑。因此在 JS 编程中,会大量使用异步来进行编程
异步的发展
回调函数--> Promise--> Generator-->async/await
回调函数
早些年为了实现 JS 的异步编程,一般都采用回调函数的方式,比如比较典型的事件的回调,或者用 setTimeout/ setInterval 来实现一些异步编程的操作,但是使用回调函数来实现存在一个很常见的问题,那就是回调地狱
fs.readFile(A, "utf-8", function(err, data) {
fs.readFile(B, "utf-8", function(err, data) {
fs.readFile(C, "utf-8", function(err, data) {
fs.readFile(D, "utf-8", function(err, data) {
//....
});
});
});
});
回调实现异步编程的场景也有很多,比如:
- ajax 请求的回调;
- 定时器中的回调;
- 事件回调;
- Nodejs 中的一些方法回调。 异步回调如果层级很少,可读性和代码的维护性暂时还是可以接受,一旦层级变多就会陷入回调地狱,上面这些异步编程的场景都会涉及回调地狱的问题
Promise
为了解决回调地狱的问题,之后社区提出了 Promise 的解决方案,ES6 又将其写进了语言标准,采用 Promise 的实现方式在一定程度上解决了回调地狱的问题
function read(url) {
return new Promise((resolve, reject) => {
fs.readFile(url, "utf8", (err, data) => {
if (err) reject(err);
resolve(data);
});
});
}
read(A)
.then((data) => {
return read(B);
})
.then((data) => {
return read(C);
})
.then((data) => {
return read(D);
})
.catch((reason) => {
console.log(reason);
});
针对回调地狱进行这样的改进,可读性的确有一定的提升,优点是可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数,但是 Promise 也存在一些问题,即便是使用 Promise 的链式调用,如果操作过多,其实并没有从根本上解决回调地狱的问题,只是换了一种写法,可读性虽然有所提升,但是依旧很难维护。不过 Promise 又提供了一个 all 方法,对于这个业务场景的代码,用 all 来实现可能效果会更好
function read(url) {
return new Promise((resolve, reject) => {
fs.readFile(url, "utf8", (err, data) => {
if (err) reject(err);
resolve(data);
});
});
}
// 通过 Promise.all 可以实现多个异步并行执行,同一时刻获取最终结果的问题
Promise.all([read(A), read(B), read(C)])
.then((data) => {
console.log(data);
})
.catch((err) => console.log(err));
Generator
Generator 也是一种异步编程解决方案,它最大的特点就是可以交出函数的执行权,Generator 函数可以看出是异步任务的容器,需要暂停的地方,都用 yield 语法来标注。Generator 函数一般配合 yield 使用,Generator 函数最后返回的是迭代器
function* gen() {
let a = yield 111;
console.log(a);
let b = yield 222;
console.log(b);
let c = yield 333;
console.log(c);
let d = yield 444;
console.log(d);
}
let t = gen();
t.next(1); //第一次调用next函数时,传递的参数无效,故无打印结果
t.next(2); // a输出2;
t.next(3); // b输出3;
t.next(4); // c输出4;
t.next(5); // d输出5;
async/await
ES6 之后 ES7 中又提出了新的异步解决方案:async/await,async 是 Generator 函数的语法糖,async/await 的优点是代码清晰(不像使用 Promise 的时候需要写很多 then 的方法链),可以处理回调地狱的问题。async/await 写起来使得 JS 的异步代码看起来像同步代码,其实异步编程发展的目标就是让异步逻辑的代码看起来像同步一样容易理解
function testWait() {
return new Promise((resolve, reject) => {
setTimeout(function() {
console.log("testWait");
resolve();
}, 1000);
});
}
async function testAwaitUse() {
await testWait();
console.log("hello");
return 123; // 输出顺序:testWait,hello // 第十行如果不使用await输出顺序:hello , testWait
}
console.log(testAwaitUse());
执行上面的代码,从结果中可以看出,在正常的执行顺序下,testWait 这个函数由于使用的是 setTimeout 的定时器,回调会在一秒之后执行,但是由于执行到这里采用了 await 关键词,testAwaitUse 函数在执行的过程中需要等待 testWait 函数执行完成之后,再执行打印 hello 的操作。但是如果去掉 await ,打印结果的顺序就会变化。 因此,async/await 不仅仅是 JS 的异步编程的一种方式,其可读性也接近于同步代码,让人更容易理解。
小结
js异步编程方式 | 简单总结 |
---|---|
回调函数 | 早些年 js 异步编程采用的方式 |
Promise | ES6 新增异步编程方式,解决回调地狱问题 |
Generator | 和 yield 配合使用,返回的是迭代器 |
async/await | 二者配合使用,async 返回的是 Promise 对象,await 控制执行顺序 |
Promise
Promise 介绍
如果一定要解释 Promise 到底是什么,简单来说它就是一个容器,里面保存着某个未来才会结束的事件(通常是异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。 Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理
function read(url) {
return new Promise((resolve, reject) => {
fs.readFile(url, "utf8", (err, data) => {
if (err) reject(err);
resolve(data);
});
});
}
read(A)
.then((data) => {
return read(B);
})
.then((data) => {
return read(C);
})
.then((data) => {
return read(D);
})
.catch((reason) => {
console.log(reason);
});
Promise 对象在被创建出来时是待定的状态,它让你能够把异步操作返回最终的成功值或者失败原因,和相应的处理程序关联起来。
一般 Promise 在执行过程中,必然会处于以下几种状态之一。
- 待定(pending):初始状态,既没有被完成,也没有被拒绝。
- 已完成(fulfilled):操作成功完成。
- 已拒绝(rejected):操作失败。
待定状态的 Promise 对象执行的话,最后要么会通过一个值完成,要么会通过一个原因被拒绝。当其中一种情况发生时,我们用 Promise 的 then 方法排列起来的相关处理程序就会被调用。因为最后
Promise.prototype.then
和Promise.prototype.catch
方法返回的是一个 Promise, 所以它们可以继续被链式调用。
关于 Promise 的状态流转情况,有一点值得注意的是,内部状态改变之后不可逆
Promise 如何解决回调地狱
回调地狱有两个主要的问题:
- 多层嵌套的问题;
- 每种任务的处理结果存在两种可能性(成功或失败),
那么需要在每种任务执行结束后分别处理这两种可能性。
这两种问题在“回调函数时代”尤为突出,Promise 的诞生就是为了解决这两个问题。Promise 利用了三大技术手段来解决回调地狱:回调函数延迟绑定、返回值穿透、错误冒泡
let readFilePromise = (filename) => {
return new Promise((resolve, reject) => {
fs.readFile(filename, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
};
readFilePromise("1.json").then((data) => {
return readFilePromise("2.json");
});
从上面的代码中可以看到,回调函数不是直接声明的,而是通过后面的 then 方法传入的,即延迟传入,这就是回调函数延迟绑定。接下来我们针对上面的代码做一下微调,如下所示
let x = readFilePromise("1.json").then((data) => {
return readFilePromise("2.json"); //这是返回的Promise
});
x.then(/* 内部逻辑省略 */);
我们根据 then 中回调函数的传入值创建不同类型的 Promise,然后把返回的 Promise 穿透到外层,以供后续的调用。这里的 x 指的就是内部返回的 Promise,然后在 x 后面可以依次完成链式调用。这便是返回值穿透的效果,这两种技术一起作用便可以将深层的嵌套回调写成下面的形式
readFilePromise("1.json")
.then((data) => {
return readFilePromise("2.json");
})
.then((data) => {
return readFilePromise("3.json");
})
.then((data) => {
return readFilePromise("4.json");
});
这样就显得清爽了许多,更重要的是,它更符合人的线性思维模式,开发体验也更好,两种技术结合产生了链式调用的效果。
这样解决了多层嵌套的问题,那另外一个问题,即每次任务执行结束后分别处理成功和失败的情况怎么解决的呢?Promise 采用了错误冒泡的方式
readFilePromise("1.json")
.then((data) => {
return readFilePromise("2.json");
})
.then((data) => {
return readFilePromise("3.json");
})
.then((data) => {
return readFilePromise("4.json");
})
.catch((err) => {
// xxx
});
这样前面产生的错误会一直向后传递,被 catch 接收到,就不用频繁地检查错误了。从上面的这些代码中可以看到,Promise 解决效果也比较明显:实现链式调用,解决多层嵌套问题;实现错误冒泡后一站式处理,解决每次任务中判断错误、增加代码混乱度的问题
Promise 的静态方法
all 方法
语法: Promise.all(iterable)
参数: 一个可迭代对象,如 Array。
描述: 此方法对于汇总多个 promise 的结果很有用,在 ES6 中可以将多个 Promise.all 异步请求并行操作,返回结果一般有下面两种情况:
当所有结果成功返回时按照请求顺序返回成功。
当其中有一个失败方法时,则进入失败方法。
我们来看下业务的场景,对于下面这个业务场景页面的加载,将多个请求合并到一起,用 all 来实现可能效果会更好,
//1.获取轮播数据列表
function getBannerList() {
return new Promise((resolve, reject) => {
setTimeout(function() {
resolve("轮播数据");
}, 300);
});
}
//2.获取店铺列表
function getStoreList() {
return new Promise((resolve, reject) => {
setTimeout(function() {
resolve("店铺数据");
}, 500);
});
}
//3.获取分类列表
function getCategoryList() {
return new Promise((resolve, reject) => {
setTimeout(function() {
resolve("分类数据");
}, 700);
});
}
function initLoad() {
Promise.all([getBannerList(), getStoreList(), getCategoryList()])
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log(err);
});
}
initLoad();
从上面代码中可以看出,在一个页面中需要加载获取轮播列表、获取店铺列表、获取分类列表这三个操作,页面需要同时发出请求进行页面渲染,这样用 Promise.all 来实现,看起来更清晰、一目了然。
allSettled 方法
Promise.allSettled 的语法及参数跟 Promise.all 类似,其参数接受一个 Promise 的数组,返回一个新的 Promise。唯一的不同在于,执行完之后不会失败,也就是说当 Promise.allSettled 全部处理完成后,我们可以拿到每个 Promise 的状态,而不管其是否处理成功。
我们来看一下用 allSettled 实现的一段代码。
const resolved = Promise.resolve(2);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.allSettled([resolved, rejected]);
allSettledPromise.then(function(results) {
console.log(results);
});
// 返回结果:
// [
// { status: 'fulfilled', value: 2 },
// { status: 'rejected', reason: -1 }
// ]
从上面代码中可以看到,Promise.allSettled 最后返回的是一个数组,记录传进来的参数中每个 Promise 的返回值,这就是和 all 方法不太一样的地方。你也可以根据 all 方法提供的业务场景的代码进行改造,其实也能知道多个请求发出去之后,Promise 最后返回的是每个参数的最终状态。
any 方法
语法: Promise.any(iterable)
参数: iterable 可迭代的对象,例如 Array。
描述: any 方法返回一个 Promise,只要参数 Promise 实例有一个变成 fulfilled 状态,最后 any 返回的实例就会变成 fulfilled 状态;如果所有参数 Promise 实例都变成 rejected 状态,包装实例就会变成 rejected 状态
还是对上面 allSettled 这段代码进行改造,我们来看下改造完的代码和执行结果。
const resolved = Promise.resolve(2);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.any([resolved, rejected]);
allSettledPromise.then(function(results) {
console.log(results);
});
// 返回结果:
// 2
从改造后的代码中可以看出,只要其中一个 Promise 变成 fulfilled 状态,那么 any 最后就返回这个 Promise。由于上面 resolved 这个 Promise 已经是 resolve 的了,故最后返回结果为 2。
race 方法
语法: Promise.race(iterable)
参数: iterable 可迭代的对象,例如 Array。
描述: race 方法返回一个 Promise,只要参数的 Promise 之中有一个实例率先改变状态,则 race 方法的返回状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 race 方法的回调函数。
我们来看一下这个业务场景,对于图片的加载,特别适合用 race 方法来解决,将图片请求和超时判断放到一起,用 race 来实现图片的超时判断。请看代码片段。
//请求某个图片资源
function requestImg() {
var p = new Promise(function(resolve, reject) {
var img = new Image();
img.onload = function() {
resolve(img);
};
img.src = "http://www.baidu.com/img/flexible/logo/pc/result.png";
});
return p;
}
//延时函数,用于给请求计时
function timeout() {
var p = new Promise(function(resolve, reject) {
setTimeout(function() {
reject("图片请求超时");
}, 5000);
});
return p;
}
Promise.race([requestImg(), timeout()])
.then(function(results) {
console.log(results);
})
.catch(function(reason) {
console.log(reason);
});
从上面的代码中可以看出,采用 Promise 的方式来判断图片是否加载成功,也是针对 Promise.race 方法的一个比较好的业务场景。
小结
Promise方法 | 简单总结 |
---|---|
all | 参数返回所有结果成功才返回 |
allSettled | 参数不论返回结果是否成功,都会返回每个参数执行状态 |
any | 参数中只要有一个成功,就返回该成功的执行结果 |
race | 顾名思义返回最先返回执行成功的参数的执行结果 |
Generator
Generator(生成器)是 ES6 的新关键词,学习起来比较晦涩难懂,那么什么是 Generator 的函数呢?通俗来讲 Generator 是一个带星号的“函数”(它并不是真正的函数,下面的代码会为你验证),可以配合 yield 关键字来暂停或者执行函数。我们来看一段使用 Generator 的代码
function* gen() {
console.log("enter");
let a = yield 1;
let b = yield (function() {
return 2;
})();
return 3;
}
var g = gen(); // 阻塞住,不会执行任何语句
console.log(typeof g); // 返回 object 这里不是 "function"
console.log(g.next());
console.log(g.next());
console.log(g.next());
console.log(g.next());
// output:
// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: true }
// { value: undefined, done: true }
Generator 中配合使用 yield 关键词可以控制函数执行的顺序,每当执行一次 next 方法,Generator 函数会执行到下一个存在 yield 关键词的位置。
总结下来,Generator 的执行有这几个关键点。
- 调用 gen() 后,程序会阻塞住,不会执行任何语句。
- 调用 g.next() 后,程序继续执行,直到遇到 yield 关键词时执行暂停。
- 一直执行 next 方法,最后返回一个对象,其存在两个属性:value 和 done。 这就是 Generator 的基本内容,其中提到了 yield 这个关键词,下面我们就来看看它的基本情况
yield yield 同样也是 ES6 的新关键词,配合 Generator 执行以及暂停。yield 关键词最后返回一个迭代器对象,该对象有 value 和 done 两个属性,其中 done 属性代表返回值以及是否完成。yield 配合着 Generator,再同时使用 next 方法,可以主动控制 Generator 执行进度。 前面说 Generator 的时候,我举的是一个生成器函数的示例,下面我们看看多个 Generator 配合 yield 使用的情况
function* gen1() {
yield 1;
yield* gen2();
yield 4;
}
function* gen2() {
yield 2;
yield 3;
}
var g = gen1();
console.log(g.next());
console.log(g.next());
console.log(g.next());
console.log(g.next());
// output:
// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: false }
// { value: 4, done: false }
// {value: undefined, done: true}
使用 yield 关键词的话还可以配合着 Generator 函数嵌套使用,从而控制函数执行进度。这样对于 Generator 的使用,以及最终函数的执行进度都可以很好地控制,从而形成符合你设想的执行顺序。即便 Generator 函数相互嵌套,也能通过调用 next 方法来按照进度一步步执行 Generator 和异步编程有什么联系?怎么才可以把 Generator 函数按照顺序一次性执行完呢?
thunk 函数介绍
通过一段代码来了解一下什么是 thunk 函数,就拿判断数据类型来举例
let isString = (obj) => {
return Object.prototype.toString.call(obj) === '[object String]';
};
let isFunction = (obj) => {
return Object.prototype.toString.call(obj) === '[object Function]';
};
let isArray = (obj) => {
return Object.prototype.toString.call(obj) === '[object Array]';
};
可以看到,其中出现了非常多重复的数据类型判断逻辑,平常业务开发中类似的重复逻辑的场景也同样会有很多。我们将它们做一下封装,如下所示。
let isType = (type) => {
return (obj) => {
return Object.prototype.toString.call(obj) === `[object ${type}]`;
};
};
那么封装了之后我们可以这么来使用,从而来减少重复的逻辑代码,如下所示。
let isString = isType("String");
let isArray = isType("Array");
isString("123"); // true
isArray([1, 2, 3]); // true
相应的 isString 和 isArray 是由 isType 方法生产出来的函数,通过上面的方式来改造代码,明显简洁了不少。像 isType 这样的函数我们称为 thunk 函数,它的基本思路都是接收一定的参数,会生产出定制化的函数,最后使用定制化的函数去完成想要实现的功能。 这样的函数在 JS 的编程过程中会遇到很多,尤其是你在阅读一些开源项目时,抽象度比较高的 JS 代码往往都会采用这样的方式。 Generator 和 thunk 函数的结合是否能为我们带来一定的便捷性呢?
Generator 和 thunk 结合
const readFileThunk = (filename) => {
return (callback) => {
fs.readFile(filename, callback);
};
};
const gen = function*() {
const data1 = yield readFileThunk("1.txt");
console.log(data1.toString());
const data2 = yield readFileThunk("2.txt");
console.log(data2.toString);
};
let g = gen();
g.next().value((err, data1) => {
g.next(data1).value((err, data2) => {
g.next(data2);
});
});
readFileThunk 就是一个 thunk 函数,上面的这种编程方式就让 Generator 和异步操作关联起来了。上面第三段代码执行起来嵌套的情况还算简单,如果任务多起来,就会产生很多层的嵌套,可读性不强,因此我们有必要把执行的代码封装优化一下
function run(gen) {
const next = (err, data) => {
let res = gen.next(data);
if (res.done) return;
res.value(next);
};
next();
}
run(g);
改造完之后,我们可以看到 run 函数和上面的执行效果其实是一样的。代码虽然只有几行,但其包含了递归的过程,解决了多层嵌套的问题,并且完成了异步操作的一次性的执行效果。这就是通过 thunk 函数完成异步操作的情况
Generator 和 Promise 结合
// 最后包装成 Promise 对象进行返回
const readFilePromise = (filename) => {
return new Promise((resolve, reject) => {
fs.readFile(filename, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
}).then((res) => res);
};
// 这块和上面 thunk 的方式一样
const gen = function*() {
const data1 = yield readFilePromise("1.txt");
console.log(data1.toString());
const data2 = yield readFilePromise("2.txt");
console.log(data2.toString);
};
// 这块和上面 thunk 的方式一样
function run(gen) {
const next = (err, data) => {
let res = gen.next(data);
if (res.done) return;
res.value.then(next);
};
next();
}
run(g);
thunk 函数的方式和通过 Promise 方式执行效果本质上是一样的,只不过通过 Promise 的方式也可以配合 Generator 函数实现同样的异步操作 # co 函数库 co 函数库是著名程序员 TJ 发布的一个小工具,用于处理 Generator 函数的自动执行。核心原理其实就是上面讲的通过和 thunk 函数以及 Promise 对象进行配合,包装成一个库,它使用起来非常简单,比如还是用上面那段代码,第三段代码就可以省略了,直接引用 co 函数,包装起来就可以使用了
const co = require("co");
let g = gen();
co(g).then((res) => {
console.log(res);
});
那么为什么 co 函数库可以自动执行 Generator 函数,它的处理原理是什么呢?
- 因为 Generator 函数就是一个异步操作的容器,它需要一种自动执行机制,co 函数接受 Generator 函数作为参数,并最后返回一个 Promise 对象。
- 在返回的 Promise 对象里面,co 先检查参数 gen 是否为 Generator 函数。如果是,就执行该函数;如果不是就返回,并将 Promise 对象的状态改为 resolved。
- co 将 Generator 函数的内部指针对象的 next 方法,包装成 onFulfilled 函数。这主要是为了能够捕捉抛出的错误。 关键的是 next 函数,它会反复调用自身。
终极解决方案 async/await
JS 的异步编程从最开始的回调函数的方式,演化到使用 Promise 对象,再到 Generator+co 函数的方式,每次都有一些改变,但又让人觉得不彻底,都需要理解底层运行机制。 而 async/await 被称为 JS 中异步终极解决方案,它既能够像 co+Generator 一样用同步的方式来书写异步代码,又得到底层的语法支持,无须借助任何第三方库
// readFilePromise 依旧返回 Promise 对象
const readFilePromise = (filename) => {
return new Promise((resolve, reject) => {
fs.readFile(filename, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
}).then((res) => res);
};
// 这里把 Generator的 * 换成 async,把 yield 换成 await
const gen = async function() {
const data1 = await readFilePromise("1.txt");
console.log(data1.toString());
const data2 = await readFilePromise("2.txt");
console.log(data2.toString);
};
虽然我们简单地将 Generator 的 * 号换成了 async,把 yield 换成了 await,但其实 async 的内部做了不少工作。我们根据 async 的原理详细拆解一下,看看它到底做了哪些工作。
总结下来,async 函数对 Generator 函数的改进,主要体现在以下三点。
- 内置执行器:Generator 函数的执行必须靠执行器,因为不能一次性执行完成,所以之后才有了开源的 co 函数库。但是,async 函数和正常的函数一样执行,也不用 co 函数库,也不用使用 next 方法,而 async 函数自带执行器,会自动执行。
- 适用性更好:co 函数库有条件约束,yield 命令后面只能是 Thunk 函数或 Promise 对象,但是 async 函数的 await 关键词后面,可以不受约束。
- 可读性更好:async 和 await,比起使用 * 号和 yield,语义更清晰明了。 说了这么多优点,我们还是通过一段简单的代码来看下 async 返回的结果,是不是使用起来更方便 async
function func() {
return 100;
}
console.log(func());
// Promise {<fulfilled>: 100}
async 函数 func 最后返回的结果直接是 Promise 对象,比较方便让开发者继续往后处理。而之前 Generator 并不会自动执行,需要通过 next 方法控制,最后返回的也并不是 Promise 对象,而是需要通过 co 函数库来实现最后返回 Promise 对象
这样看来,ES7 加入的 async/await 的确解决了之前的问题,使开发者在编程过程中更容易理解,语法更清晰,并且也不用再单独引用 co 函数库了。因此用 async/await 写出的代码也更加优雅,相比于之前的 Promise 和 co+Generator 的方式更容易理解,上手成本也更低,不愧是 JS 异步的终极解决方案
小结
异步编程方法 | 特点 |
---|---|
Generator | 生成器函数配合着 yield 关键词使用,不自动执行,需要执行 next 方法一步一步往下执行 |
Generator+co | 通过引入开源 co 函数库,实现异步编程,并且还能控制返回结果为 Promise 对象,方便后续继续操作,但要求 yield 后面,只能是 thunk 函数或者 Promise 对象 |
async/await | ES7 引入的终极异步编程解决方案,不用引入去其它任何库,对于 await 后面的类型无限制,可读性更好,容易理解 |
Nodejs 中的异步
在听到 nodejs 相关的特性时,经常会对 异步 I/O、非阻塞 I/O 有所耳闻,听起来好像是差不多的意思,但其实是两码事,下面我们就以原理的角度来剖析一下对 nodejs 来说,这两种技术底层是如何实现的
什么是 I/O
首先,我想有必要把 I/O 的概念解释一下。I/O 即 Input/Output, 输入和输出的意思。在浏览器端,只有一种 I/O,那就是利用 Ajax 发送网络请求,然后读取返回的内容,这属于网络 I/O。回到 nodejs 中,其实这种的 I/O 的场景就更加广泛了,主要分为两种:
- 文件 I/O。比如用 fs 模块对文件进行读写操作
- 网络 I/O。比如 http 模块发起网络请求
阻塞和非阻塞
I/O 阻塞和非阻塞 I/O 其实是针对操作系统内核而言的,而不是 nodejs 本身。阻塞 I/O 的特点就是一定要等到操作系统完成所有操作后才表示调用结束,而非阻塞 I/O 是调用后立马返回,不用等操作系统内核完成操作。
对前者而言,在操作系统进行 I/O 的操作的过程中,我们的应用程序其实是一直处于等待状态的,什么都做不了。那如果换成非阻塞 I/O,调用返回后我们的 nodejs 应用程序可以完成其他的事情,而操作系统同时也在进行 I/O。这样就把等待的时间充分利用了起来,提高了执行效率,但是同时又会产生一个问题,nodejs 应用程序怎么知道操作系统已经完成了 I/O 操作呢?
为了让 nodejs 知道操作系统已经做完 I/O 操作,需要重复地去操作系统那里判断一下是否完成,这种重复判断的方式就是轮询。对于轮询而言,有以下这么几种方案:
- 一直轮询检查 I/O 状态,直到 I/O 完成。这是最原始的方式,也是性能最低的,会让 CPU 一直耗用在等待上面。其实跟阻塞 I/O 的效果是一样的。
- 遍历文件描述符(即 文件 I/O 时操作系统和 nodejs 之间的文件凭证)的方式来确定 I/O 是否完成,I/O 完成则文件描述符的状态改变。但 CPU 轮询消耗还是很大。
- epoll 模式。即在进入轮询的时候如果 I/O 未完成 CPU 就休眠,完成之后唤醒 CPU。
总之,CPU 要么重复检查 I/O,要么重复检查文件描述符,要么休眠,都得不到很好的利用,我们希望的是: nodejs 应用程序发起 I/O 调用后可以直接去执行别的逻辑,操作系统默默地做完 I/O 之后给 nodejs 发一个完成信号,nodejs 执行回调操作。
这是理想的情况,也是异步 I/O 的效果,那如何实现这样的效果呢?
异步 I/O 的本质
Linux 原生存在这样的一种方式,即(AIO), 但两个致命的缺陷:
- 只有 Linux 下存在,在其他系统中没有异步 I/O 支持。
- 无法利用系统缓存。 nodejs 中的异步 I/O 方案
是不是没有办法了呢?在单线程的情况下确实是这样,但是如果把思路放开一点,利用多线程来考虑这个问题,就变得轻松多了。我们可以让一个进程进行计算操作,另外一些进行 I/O 调用,I/O 完成后把信号传给计算的线程,进而执行回调,这不就好了吗?没错,异步 I/O 就是使用这样的线程池来实现的。
只不过在不同的系统下面表现会有所差异,在 Linux 下可以直接使用线程池来完成,在 Window 系统下则采用 IOCP 这个系统 API(其内部还是用线程池完成的)。
有了操作系统的支持,那 nodejs 如何来对接这些操作系统从而实现异步 I/O 呢? 以文件为 I/O 我们以一段代码为例:
let fs = require("fs");
fs.readFile("/test.txt", function(err, data) {
console.log(data);
});
执行流程 执行代码的过程中大概发生了这些事情:
- 首先,fs.readFile 调用 Node 的核心模块 fs.js
- 接下来,Node 的核心模块调用内建模块 node_file.cc,创建对应的文件 I/O 观察者对象(这个对象后面有大用!)
- 最后,根据不同平台(Linux 或者 window),内建模块通过 libuv 中间层进行系统调用
libuv 调用过程拆解
重点来了!libuv 中是如何来进行进行系统调用的呢?也就是 uv_fs_open() 中做了些什么?
- 创建请求对象 以 Windows 系统为例来说,在这个函数的调用过程中,我们创建了一个文件 I/O 的请求对象,并往里面注入了回调函数。
req_wrap->object_->Set(oncomplete_sym, callback);
1req_wrap 便是这个请求对象,req_wrap 中 object_ 的 oncomplete_sym 属性对应的值便是我们 nodejs 应用程序代码中传入的回调函数。
-
推入线程池,调用返回 在这个对象包装完成后,QueueUserWorkItem() 方法将这个对象推进线程池中等待执行。
好,至此现在 js 的调用就直接返回了,我们的 js 应用程序代码可以继续往下执行,当然,当前的 I/O 操作同时也在线程池中将被执行,这不就完成了异步么:)
等等,别高兴太早,回调都还没执行呢!接下来便是执行回调通知的环节。 -
回调通知 事实上现在线程池中的 I/O 无论是阻塞还是非阻塞都已经无所谓了,因为异步的目的已经达成。重要的是 I/O 完成后会发生什么。
在介绍后续的故事之前,给大家介绍两个重要的方法: GetQueuedCompletionStatus 和 PostQueuedCompletionStatus。
在每一个 Tick(一轮事件循环) 当中会调用 GetQueuedCompletionStatus 检查线程池中是否有执行完的请求,如果有则表示时机已经成熟,可以执行回调了。
PostQueuedCompletionStatus 方法则是向 IOCP 提交状态,告诉它当前 I/O 完成了。 当对应线程中的 I/O 完成后,会将获得的结果存储起来,保存到相应的请求对象中,然后调用 PostQueuedCompletionStatus()向 IOCP 提交执行完成的状态,并且将线程还给操作系统。一旦 EventLoop 的轮询操作中,调用 GetQueuedCompletionStatus 检测到了完成的状态,就会把请求对象塞给 I/O 观察者(之前埋下伏笔,如今终于闪亮登场)。
I/O 观察者现在的行为就是取出请求对象的存储结果,同时也取出它的 oncomplete_sym 属性,即回调函数(不懂这个属性的回看第 1 步的操作)。将前者作为函数参数传入后者,并执行后者。 这里,回调函数就成功执行啦!
小结
- 阻塞和非阻塞 I/O 其实是针对操作系统内核而言的。阻塞 I/O 的特点就是一定要等到操作系统完成所有操作后才表示调用结束,而非阻塞 I/O 是调用后立马返回,不用等操作系统内核完成操作。
- nodejs 中的异步 I/O 采用多线程的方式,由 EventLoop、I/O 观察者,请求对象、线程池四大要素相互配合,共同实现。
this
this 是 JavaScript 的一个关键字,一般指向调用它的对象,这句话其实有两层意思,首先 this 指向的应该是一个对象,更具体地说是函数执行的“上下文对象”。其次这个对象指向的是“调用它”的对象,如果调用它的不是对象或对象不存在,则会指向全局对象(严格模式下为 undefined)。 、
四种调用模式
this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断
- 函数调用模式:fn(),this 指向全局对象
- 方法调用模式,a.fn,this 指向这个对象
- 构造器调用模式:var f = a.fn(),this 指向这个新创建的对象
- apply 、 call 和 bind 调用模式
显式绑定 (call, apply, bind)
- call和apply的区别
call:逐个参数传递
apply:将参数放入一个数组,以数组的形式传递
bind 有些特殊,它不但可以绑定 this 指向也可以绑定函数参数并返回一个新的函数,当 c 调用新的函数时,绑定之后的 this 或参数将无法再被改变
function getName() {
console.log(this.name);
}
var b = getName.bind({ name: "bind" });
b(); //"bind"
getName.call({ name: "call" }); //"call"
getName.apply({ name: "apply" }); //"apply"
提示 由于 this 指向的不确定性,所以很容易在调用时发生意想不到的情况。在编写代码时,应尽量避免使用 this,比如可以写成纯函数的形式,也可以通过参数来传递上下文对象。实在要使用 this 的话,可以考虑使用 bind 等方式将其绑定
隐式绑定 this
全局上下文
function fn() {
console.log(this);
}
fn(); // 浏览器:Window;Node.js:global
全局上下文默认 this 指向 window, 严格模式下指向 undefined
直接调用函数
let obj = {
a: function() {
console.log(this);
},
};
let func = obj.a;
func();
这种情况是直接调用。this 相当于全局上下文的情况
对象.方法的形式调用
let obj = {
a: function() {
console.log(this);
},
};
let func = obj.a();
func();
这种情况 this 指向这个对象
DOM 事件绑定
onclick 和 addEventerListener 中 this 默认指向绑定事件的元素。 IE 比较奇异,使用 attachEvent,里面的 this 默认指向 window。
new + 构造函数 此时构造函数中的 this 指向实例对象。
class A {
fn() {
console.log(this);
}
}
var a = new A();
a.fn(); // A{}
箭头函数
箭头函数不会创建自己的 this,它只会从自己的作用域链的上一层继承 this。可以简单地理解为箭头函数的 this 继承自上层的 this,但在全局环境下定义仍会指向全局对象
let obj = {
a: function() {
const fun = () => {
console.log(this);
};
fun();
},
};
obj.a(); // 找到最近的非箭头函数a,a现在绑定着obj, 因此箭头函数中的this是obj
箭头函数与普通函数的区别
箭头函数和普通函数相比,有以下几个区别
- 不绑定 arguments 对象,也就是说在箭头函数内访问 arguments 对象会报错;
- 不能用作构造器,也就是说不能通过关键字 new 来创建实例;
- 默认不会创建 prototype 原型属性;
- 不能用作 Generator() 函数,不能使用 yeild 关键字。
一句很经典的话
正常情况:this 指向运行时所在的对象而不是定义时所在对象
箭头函数:this 指向定义时所在的对象而不是使用时所在对象
优先级
优先级: new > call、apply、bind > 对象.方法 > 直接调用
稍复杂点的例子
function fn() {
console.log(this);
}
function fn2() {
fn();
}
fn2(); // ?
由于没有找到调用 fn 的对象,所以 this 会指向全局对象,答案就是 window(Node.js 下是 global)
function fn() {
console.log(this);
}
function fn2() {
fn();
}
var obj = { fn2 };
obj.fn2(); // ?
调用函数 fn() 的是函数 fn2() 而不是 obj。虽然 fn2() 作为 obj 的属性调用,但 fn2()中的 this 指向并不会传递给函数 fn(), 所以答案也是 window(Node.js 下是 global)
var dx = {
arr: [1],
};
dx.arr.forEach(function() {
console.log(this);
}); // ?
关于 forEach,它有两个参数,
第一个是回调函数,
第二个是 this 指向的对象,
这里只传入了回调函数,第二个参数没有传入,默认为 undefined,
所以正确答案应该是输出全局对象。 提示 类似的,需要传入 this 指向的函数还有:every()、find()、findIndex()、map()、some(),在使用的时候需要特别注意。 创建一个 fun 变量来引用实例 b 的 fn() 函数,当调用 fun() 的时候 this 会指向什么呢?
class B {
fn() {
console.log(this);
}
}
var b = new B();
var fun = b.fn;
fun(); // ?
fun 是在全局下调用的,所以 this 应该指向的是全局对象。这个思路没有没问题,但是 ES6 下的 class 内部默认采用的是严格模式,实际上面代码的类定义部分可以理解为下面的形式。
class B {
"use strict";
fn() {
console.log(this);
}
}
而严格模式下不会指定全局对象为默认调用对象,所以答案是 undefined。 前面提到 this 指向的要么是调用它的对象,要么是 undefined,那么如果将 this 指向一个基础类型的数据会发生什么呢 ```
[0].forEach(function() {
console.log(this);
}, 0); //Number {0}
基础类型也可以转换成对应的引用对象。所以这里 this 指向的是一个值为 0 的 Number 类型对象