如图所示,JS的三座大山:
- 同步、异步
- 作用域、闭包
- 原型、原型链
1. 同步、异步
JavaScript执行机制,重点有两点:
- JavaScript是一门单线程语言
- Event Loop(事件循环)是JavaScript的执行机制
JS为什么是单线程
js为什么需要异步
JS的事件循环(eventloop)是怎么运作的
-
首先判断JS是同步还是异步,同步就进入主线程运行,异步就进入event table.
-
异步任务在event table中注册事件,当满足触发条件后,(触发条件可能是延时也可能是ajax回调),被推入event queue
-
同步任务进入主线程后一直执行,直到主线程空闲时,才会去event queue中查看是否有可执行的异步任务,如果有就推入主线程中。
如图所示:
宏任务 包含整个script代码块,setTimeout, setIntval
微任务 Promise , process.nextTicksetTimeout(function() { console.log('4') }) new Promise(function(resolve) { console.log('1') // 同步任务 resolve() }).then(function() { console.log('3') }) console.log('2') 执行结果: 1-2-3-4 1. 这段代码作为宏任务,进入主线程。 2. 先遇到setTimeout,那么将其回调函数注册后分发到宏任务event queue. 3. 接下来遇到Promise, new Promise立即执行,then函数分发到微任务event queue 4. 遇到console.log(), 立即执行 5. 整体代码script作为第一个宏任务执行结束, 查看当前有没有可执行的微任务,执行then的回调。(第一轮事件循环结束了,我们开始第二轮循环) 6. 从宏任务的event queue开始,我们发现了宏任务event queue中setTimeout对应的回调函数,立即执行。
console.log('1') setTimeout(function() { console.log('2') process.nextTick(function() { console.log('3') }) new Promise(function(resolve) { console.log('4') resolve() }).then(function() { console.log('5') }) }) process.nextTick(function() { console.log('6') }) new Promise(function(resolve) { console.log('7') resolve() }).then(function() { console.log('8') }) setTimeout(function() { console.log('9') process.nextTick(function() { console.log('10') }) new Promise(function(resolve) { console.log('11') resolve() }).then(function() { console.log('12') }) }) 1.整体script作为第一个宏任务进入主线程,遇到console.log(1)输出1 遇到setTimeout, 其回调函数被分发到宏任务event queue中。我们暂且记为setTimeout1 3.遇到process.nextTick(),其回调函数被分发到微任务event queue中,我们记为process1 4.遇到Promise, new Promise直接执行,输出7.then被分发到微任务event queue中,我们记为then1 又遇到setTimeout,其回调函数被分发到宏任务event queue中,我们记为setTimeout2. 现在开始执行微任务, 我们发现了process1和then1两个微任务,执行process1,输出6,执行then1,输出8, 第一轮事件循环正式结束, 这一轮的结果输出1,7,6,8.那么第二轮事件循环从setTimeout1宏任务开始 5. 首先输出2, 接下来遇到了process.nextTick(),统一被分发到微任务event queue,记为process2 8new Promise立即执行,输出4,then也被分发到微任务event queue中,记为then2 6. 现在开始执行微任务,我们发现有process2和then2两个微任务可以执行输出3,5. 第二轮事件循环结束,第二轮输出2,4,3,5. 第三轮事件循环从setTimeout2哄任务开始 10。 直接输出9,跟第二轮事件循环类似,输出9,11,10,12 7. 完整输出是1,7,6,8,2,4,3,5,9,11,10,12(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)
async/await用来干什么
async/await内部做了什么
await的等待机制
async/await在使用过程中有什么规定
async/await在什么场景使用
2. 作用域、闭包
闭包
作用域
let a = 1;
function f1() {
var a = 2
function f2() {
var a = 3;
console.log(a); //3
}
}
在这段代码中,
- f1的作用域指向有全局作用域(window) 和它本身,
- 而f2的作用域指向全局作用域(window)、 f1和它本身。
- 而且作用域是从最底层向上找, 直到找到全局作用域window为止,
- 如果全局还没有的话就会报错。闭包产生的本质就是,
- 当前环境中存在指向父级作用域的引用。
function f2() {
var a = 2
function f3() {
console.log(a); //2
}
return f3;
}
var x = f2();
x();
这里x会拿到父级作用域中的变量, 输出2。
var f4;
function f5() {
var a = 2
f4 = function () {
console.log(a);
}
}
f5();
f4();
场景
- 返回一个函数。
- 作为函数参数传递。
- 在定时器、 事件监听、 Ajax请求、 跨窗口通信、 Web Workers或者任何异步中,只要使用了回调函数, 实际上就是在使用闭包。
IIFE(立即执行函数表达式) 创建闭包, 保存了全局作用域window和当前函数的作用域。
var b = 1;
function foo() {
var b = 2;
function baz() {
console.log(b);
}
bar(baz);
}
function bar(fn) {
// 这就是闭包
fn();
}
// 输出2,而不是1
foo();
// 以下的闭包保存的仅仅是window和当前作用域。
// 定时器
setTimeout(function timeHandler() {
console.log('111');
}, 100)
// 事件监听
// document.body.click(function () {
// console.log('DOM Listener');
// })
// 立即执行函数
var c = 2;
(function IIFE() {
// 输出2
console.log(c);
})();
经典的一道题
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, 0)
} // 6 6 6 6 6 6
// 为什么会全部输出6? 如何改进, 让它输出1, 2, 3, 4, 5?
解析:
- 因为setTimeout为宏任务, 由于JS中单线程eventLoop机制, 在主线程同步任务执行完后才去执行宏任务。
- 因此循环结束后setTimeout中的回调才依次执行, 但输出i的时候当前作用域没有。
往上一级再找,发现了i,此时循环已经结束,i变成了6,因此会全部输出6。
利用IIFE(立即执行函数表达式)当每次for循环时,把此时的i变量传递到定时器中
for (var i = 0; i < 5; i++) {
(function (j) {
setTimeout(() => {
console.log(j)
}, 1000);
})(i)
}
给定时器传入第三个参数, 作为timer函数的第一个函数参数
for (var i = 0; i < 5; i++) {
setTimeout(function (j) {
console.log(j)
}, 1000, i);
}
使用ES6中的let
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, 2000)
}
3.原型、原型链
原型(prototype)
代码如下:
function Person(name){
this.name = name
}
Person.prototype.sayHello(){
console.log('sayHello')
}
let p1 = new Person();
let p2 = new Person();
console.log(p1.sayHello) //sayHello
console.log(p2.sayHello) //sayHello
构造函数(constructor)
Person.prototype.constructor ==Person //true
原型链(_ _ proto _ _)
p1.__proto__ == Person.prototype
有几个面试经常会问的几个问题
如何精确地判断短数组的类型
[] instanceof Array //[].__proto__ == Array.prototype
Object.prototype.toString.call([]) //[Object Array]
Array.isArray([]) //true
[].constructor ==Array
下面代码输出什么
Object instanceof Function //true
Function instanceof Object // true
实现一个原型链继承
function Person(name){
this.name = name
}
Person.prototype.sayHello(){
console.log('sayHello')
}
function Boy(){};
Boy.prototype = new Person();
let b1 = new Boy();
b1.sayHello() //sayHello
原型、原型链、构造函数、实例的关系
1.instanceof检测构造函数与实例的关系:
function Person () {.........}
person = new Person ()
res = person instanceof Person
res // true
2.实例继承原型上的定义的属性:
function Person () {........}
Person.prototype.type = 'object n'
person = new Person ()
res = person.type
res // object n
3.实例访问 ===> 原型
实例通过__proto__访问到原型 person.proto=== Person.prototype
4.原型访问 ===> 构造函数
原型通过constructor属性访问构造函数 Person.prototype.constructor === Person
5.实例访问===>构造函数
person.proto.constructor === Person
二、原型链
- 实例上寻找
function Person() {}
Person.prototype.type = "object name Person";
person = new Person();
person.type = "我是实例的自有属性";
res = Reflect.ownKeys(person); //尝试获取到自有属性
console.log(res);
res = person.type;
console.log(res); //我是实例的自有属性(通过原型链向上搜索优先搜索实例里的)
- 原型上寻找
function Person() {}
Person.prototype.type = "object name Person";
person = new Person();
res = Reflect.ownKeys(person); //尝试获取到自有属性
console.log(res);
res = person.type;
console.log(res); //object name Person
- 原型的原型上寻找
function Person() {}
Person.prototype.type = "object name Person";
function Child() {}
Child.prototype = new Person();
p = new Child();
res = [p instanceof Object, p instanceof Person, p instanceof Child];
console.log(res); //[true, true, true] p同时属于Object,Person, Child
res = p.type; //层层搜索
console.log(res); //object name Person (原型链上搜索)
console.dir(Person);
console.dir(Child);
- 原型链上搜索
-
原型同样也可以通过 proto 访问到原型的原型,比方说这里有个构造函数 Child 然后“继承”前者的有一个构造函数 Person,然后 new Child 得到实例 p;
-
当访问 p 中的一个非自有属性的时候,就会通过 proto 作为桥梁连接起来的一系列原型、原型的原型、原型的原型的原型直到 Object 构造函数为止;
-
原型链搜索搜到 null 为止,搜不到那访问的这个属性就停止:
function Person() {}
Person.prototype.type = "object name Person";
function Child() {}
Child.prototype = new Person();
p = new Child();
res = p.__proto__;
console.log(res); //Person {}
res = p.__proto__.__proto__;
console.log(res); //Person {type:'object name Person'}
res = p.__proto__.__proto__.__proto__;
console.log(res); //{.....}
res = p.__proto__.__proto__.__proto__.__proto__;
console.log(res); //null
继承
- JS 中一切皆对象(所有的数据类型都可以用对象来表示),必须有一种机制,把所有的对象联系起来,实现类似的“继承”机制。
- 不同于大部分面向对象语言,ES6 之前并没有引入类(class)的概念,JS 并非通过类而是通过构造函数来创建实例,javascript中的继承是通过原型链来体现的。
- 其基本思想是利用原型让一个引用类型继承另一个引用继承的属性和方法。
什么是继承
- js中,继承是一种允许我们在已有类的基础上创建新类的机制;它可以使用现有类的所有功能,并在无需重新编写
- 原来的类的情况下对这些功能进行扩展。
为什么要有继承
- 提高代码的重用性、较少代码的冗余
目前我总结的一共有6种继承方式
- 原型链继承,
- 借用构造函数继承,
- 组合式继承(原型链+构造函数),
- 原型式继承,
- 寄生式继承,
- 寄生组合式继承
function Person(name){
this.name = name;
this.sum=function(){
alert('this.name',this.name)
}
}
Person.prototype.age = 100
-
原型链继承
实现方式: 利用原型链的特点继承,让实例的原型等于父类的实例
优点: 实例可以继承父类的构造个函数,实例的构造函数,父类的原型
缺点: 不能向父类传递参数,由于实例的原型等于父类的实例,那么改变父类的属性,实例的属性也会跟着改变
function child(){
this.name="xiaoming"
}
child.prototype = new Person()
let child1 = new Child()
child1.name //xiaoming
child1.age //100
child1 instanceof Person //true
-
借用构造函数继承
实现方式: 使用call/apply将父类的构造函数引入子类函数
优点: 可以祢补原型链继承的缺点,可以向父类传递参数,只继承父类构造函数的属性
缺点: 不能复用,每次使用需要重新调用,每个实例都是父类构造函数的副本,比较臃肿
function child(){
Person.call(this,'xiaoming')
}
let child1 = new child()
child1.name //xiaoming
child1.age //100
child1 instanceof Person //false
-
组合式继承
实现方式: 复用+可传递参数
优点: 基于原型链的优点和借用构造函数的优点
缺点: 调用两遍父类函数
function child(){
Person.call(this,'xiaoming')
}
child.prototype = new Person
let child1 = new child()
child1.name //xiaoming
child1.age //100
child1 instanceof Person //true
child instanceof Person //false
-
原型式继承
实现方式: 函数包装对象,返回对象的引用,这个函数就变成可以随时添加实例或者对象,Object.create()就是这个原理
优点: 复用一个对象用函数包装
缺点: 所有实例都继承在原型上面 无法复用
function child(obj){
function F(){}
F.prototype = obj
return new F()
}
let child1 = new Person()
let child2 = child(child1)
child2.age //100
-
寄生式继承
实现方式: 在原型式继承外面包了一个壳子
优点: 创建一个新对象
缺点: 没有用到实例 无法复用
function child(obj){
function F(){}
F.prototype = obj
return new F()
}
let child1 = new Person()
function subObject(){
let sub =child(child1)
sub.name='xiaoming'
return sub
}
let child2 = subObject(child1)
typeof subObject //function
typeof child2 //object
child2.age //100
-
寄生组合式继承
实现方式: 在函数内返回对象的调用
优点: 函数的实例等于另外的一个实例,使用call/apply引入另一个构造函数,可传递参数,修复了组合继承的问题
缺点: 无法复用
function child(obj){
function F(){}
F.prototype = obj
return new F()
}
let child1 = child(Person.prototype)
function Sub(){
Person.call(this)
}
Sub.prototype = child
child.constructor = Sub
let sub1 = new Sub()
sub1.age //100