- 变量类型和计算
变量类型
- String
- Number
- Boolean
- Object
- Null
- Undefined
- Symbol
常见值类型
let a //undefined
let s = 'abc' // String
let n = 100 // Number
let b = true // Boolean
let s = Symbol('s') // Symbol
常见引用类型
let Obj = { x:100 }
let arr = ['a','b','c']
let n = null //特殊引用类型,指针指向为空地址
function fn(){} //特殊引用类型,但不用于存储数据,所以没有“拷贝,复制函数”这一说
值类型和引用类型的区别
- 值类型
- 值类型的值直接存储在栈中,各不相关,如图:
- 引用类型
- 引用类型的值存储的是一个内存地址,这个内存地址指向于那个对象(堆中)栈从上往下排列,堆从下往上排列,如图:
为什么会区分这两种类型
- 根据内存空间和cpu耗时考虑规划出来的解决方法
- 如果像值类型一样直接存储复制值,会出现占内存、耗时等问题
面试题举例
- 例1
let a = 100 let b = a a = 200 console.log(b) //100 - 例2
let a = {age:20} let b = a b.age = 30 console.log(b.age) //30
typeof 运算符
作用
- 识别所有的值类型
- 识别函数
- 判断是否是引用类型(弊端:不可再细分)
- 引申(
Object.prototype.toString.call()) - 引申 (
instanceof) —— 基于原型链实现的 - 引申(
Reflect.apply(toString,[],[]))
- 引申(
用法
//判断所有的值类型
let a; typeof a // undefined
let s = 'abc'; typeof s // string
let n = 100; typeof n // number
let b = true; typeof b // boolean
let s = Symbol('s'); typeof s // Symbol
//判断函数
typeof console.log // function
typeof function(){} // function
//能识别引用类型(不能再细分)
typeof null // object
typeof ['a','b'] // object
typeof { x:100 } // object
深拷贝——手写
Object.assign(有个坑),不是深拷贝
- 只深拷贝第一层key,val数据,多层拷贝的是引用地址
原理图
/**
* 深拷贝
* @param {*} obj 要拷贝的对象
* @returns
*/
function deepClone(obj={}){
if(typeof obj !='object' || obj != null){
// obj如果不是对象或者数组,是null,直接返回
return obj
}
// 初始化返回结果
let result
if(obj instanceof Array){
result = []
}else{
result = {}
}
for(let key in obj){
// 保证 key 不是原型的属性
if(obj.hasOwnProperty(key)){
// 递归调用!!!!
result[key] = deepClone(obj[key])
}
}
// 返回结果
return result
}
let obj1 = {
age:20,
name:'xxx',
address:{
city:'beijing'
},
arr:['a','b','c']
}
let obj2 = deepClone(obj1)
obj2.age = 21;
console.log(obj1.age) // 20;
变量计算-类型转换
字符串拼接
- 任何类型 + 字符串类型 均会转换为字符串拼接
const a = 100 + 10 // 110
const b = 100 + '10' // '10010'
const c = true + '10' // 'true10'
== 运算符——不严格模式(先做类型转换在比较值)
100 == '100' // true
0 == '' // true
0 == false // true
false == '' // true
null == undefined //true
=== 运算符——严格模式(即判定值也判定类型)
- 使用严格模式,上面的结果都为false。
if语句和逻辑运算
- truly 变量:!!a === true 的变量 (经过两次取反结果为true的变量)
- falsely 变量:!!a === false 的变量
!!0 === false !!NaN === false !!'' === false !!null === false !!undefined === false !!false === false
逻辑判断
console.log(10 && 0) // 0
console.log(''||'abc') // 'abc'
console.log(!window.abc) // true
面试题举例
- typeof 能判断哪些类型
- 何时使用 === 何时使用 ==
- 值类型和引用类型的区别
const obj1 = { x: 100, y: 200};
const obj2 = obj1;
let x1 = obj1.x;
obj2.x = 101;
x1 = 102;
console.log(obj1.x); //101
- 引申-判断数据类型有几种方法,分别是什么
- typeof
- instanceof
- Object.prototype.toString.call()
- 根据对象的constructor 判断
- jQ封装判断类型方法
jQuery.isArray();是否为数组\ jQuery.isEmptyObject();是否为空对象 (不含可枚举属性)。\ jQuery.isFunction():是否为函数\ jQuery.isNumberic():是否为数字\ jQuery.isPlainObject():是否为使用“{}”或“new Object”生成对象,而不是浏览器原生提供的对象。\ jQuery.isWindow(): 是否为window对象;\ jQuery.isXMLDoc(): 判断一个DOM节点是否处于XML文档中。
- 原型和原型链
- Js本身是基于原型继承的面向对象语言。
如何用class实现继承
class的由来
- JavaScript属于弱类型的语言,在JS中并没有像Java中的那样的类的概念,ES6的Class实际上基于JS中的原型属性prototype,改良出来的语法糖
class写法
class Student {
constructor(name,age){
this.name = name
this.age = age
}
sayHi(){
console.log('hello')
}
}
let wang = new Student('wang',18)
console.log(wang.age) // 18
class实质
- 其实class类实质上就是一个函数。类自身指向的就是构造函数。所以可以认为ES6中的类其实就是构造函数的另外一种写法. 类的所有方法都定义在类的prototype属性上面
construtor
- constructor 方法是类的构造函数的默认方法,通过new 命令生成实例对象时,自动调用该方法。
- constructor方法如果没有显式定义,会隐式生成一个constructor方法。所以即使你没有添加构造函数,构造函数也是存在的。constructor方法默认返回实例对象this,但是也可以指定constructor方法返回一个全新的对象,让返回的实例对象不是该类的实例。
继承
- 继承的六种方式(区别详见)
- 原型链继承
- 借用构造函数继承
- 组合继承(常用但不是最好用的)
- 原型式继承
- 寄生式继承
- 寄生组合继承
- ES6:class继承
- ES5:prototype继承
- class继承实现方式
class People {
constuctor(name){
this.name = name
}
eat(){
console.log('eat someting')
}
}
class Student extends People {
constuctor(name,nubmer){
super(name)
this.nubmer = nubmer
}
sayHi(){
console.log('heillo')
}
}
let xialuo = new Sutdent('夏洛',100)
原型
- 何为原型:如图,我们定义了一个构造函数,而每一个构造函数中都有一个prototype属性,这就是原型。他是一个指针,指向了原型对象。
- 白话:存储共有属性和方法的对象
- 原型有几种
- 显式原型
prototype - 隐式原型
__proto__
- 显式原型
- 原型关系
- 每个实例都有隐式原型
__proto__ - 每个class都有显式原型
prototype - 每个实例的隐式原型指向new class的显式原型
- 每个class的隐式原型指向他继承的对象的显式原型
- 默认javascript会将所有的对象将Object设置为最顶级(创始人),默认所有对象都会继承自Object
console.log(People.prototype === Student.prototype.__proto__) // true - 每个实例都有隐式原型
- 原型关系图
- 基于原型的执行规则
- 获取属性xialuo.name 或 执行方法xialuo.sayhi()时
- 先在自身属性和方法中寻找
- 如果找不到则自动去
__proto__中查找,即到上一级的显式原型prototype中查找 - 如果在找不到,会继续向上查找,直到找到
Object.__proto__返回null
原型链
- 何为原型链
-
当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会去它的__proto__隐式原型上查找,即它的构造函数的prototype,如果还没有找到就会再在构造函数的prototype的__proto__中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链。
-
在JavaScript 中,每个对象都有一个指向它的原型(prototype)对象的内部链接。这个原型对象又有自己的原型,直到某个对象的原型为 null 为止(也就是不再有原型指向),组成这条链的最后一环。这种一级一级的链结构就称为原型链
-
- 装逼原型链图解
面试题举例
- 如何准确判定一个变量是不是数组
- 手写一个简易的JQuery,考虑插件和扩展性
- class的本质,怎么理解
- 作用域和闭包
作用域和自由变量
作用域
- 何为作用域
- 作用域就是变量的适用范围
- 如图:
-
作用域类型
类型 对象 全局作用域 global/window 函数作用域 function 动态作用域 this 块级作用域(ES6新增) {} -
作用域链:
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。 -
作用域举例
// ES6 块级作用域
if(true){
let x = 100
}
console.log(x) //会报错 x未定义
自由变量
- 何为自由变量
- 凡是跨了自己的作用域的变量都叫自由变量。
- 执行原理
- 一个变量在当前作用域没有定义,但被使用了
- 向上级作用域,一层层依次寻找,直到找到为止
- 如果到全局作用域都没找到,则报错 xx is not defined
- 面试题举例
var aa = 22; function a(){ console.log(aa); } function b(fn){ var aa = 11; fn(); } b(a); //22 // 在创建这个函数的时候,这个函数的作用域就已经决定了,而是不是在调用的时候。
闭包
闭包概念
- 闭包是指:有权访问另一个函数作用域中的变量的函数;
闭包原理:利用了作用域链的特性
- 原理举例
var age = 18; function cat(){ age++; console.log(age);// cat函数内输出age,该作用域没有,则向外层寻找,结果找到了,输出[19]; } cat();//19 // 如果我们再次调用时,结果会一直增加,也就变量age的值一直递增。 cat();//20 cat();//21 cat();//22 // 那么问题来了: // 如果程序还有其他函数,也需要用到age的值,则会受到影响,而且全局变量还容易被人修改,比较不安全,这就是全局变量容易污染的原因, // 所以我们必须解决变量污染问题,那就是把变量封装到函数内,让它成为局部变量。如: function person(){ var age = 18; function cat(){ age++; console.log(age); } return cat; } person()();// 19 person()();// 19 // 那么问题又来了: // 每次调用函数person,进入该作用域,变量age就会重新赋值为18,所以cat的值一直是19; // 所以需要做一下调整: var per = person();//per相当于函数cat per();// 19 即cat() 这样每次调用不在经过age的初始值,这样就可以一直增加了 per();// 20 per();// 21 // 而且变量age在函数内部,不易修改和外泄,相对来说比较安全。
闭包的表现形式
- 函数作为参数被传递
function print(fn){
let a = 200
fn()
}
let a = 100
function fn(){
console.log(a)
}
print(fn) //100
- 函数作为返回值被返回
function create(){
let a =100
return function(){
console.log(a)
}
}
let fn = create()
let a = 200
fn() //100
闭包的作用
- 优点:
- 隐藏变量,避免全局污染
- 可以读取函数内部的变量
- 缺点
- 导致变量不会被垃圾回收机制回收,造成内存消耗
- 不恰当的使用闭包可能会造成内存泄漏的问题
闭包的应用
- 需求:实现变量a 自增
var a = 10; function Add3(){ var a = 10; return function(){ a++; return a; }; }; var cc = Add3(); console.log(cc()); // 11 console.log(cc()); // 12 console.log(cc()); // 13 console.log(a); // 10 - 闭包隐藏数据,只提供api
function createCache(){ const data = {} //闭包中的数据,被隐藏,不被外界访问 return { set:function(key,val){ data[key] = val } get: function(key){ return data[key] } } } const c = createCache() c.set(a,100) console.log(c.get(a)) //100 - 定时器
function wait(message) { setTimeout(function timer() { //延时函数回调函数timer //timer内部函数具有涵盖wait函数作用域的闭包,还有对变量message的引用 console.log(message); }, 1000) } wait('闭包函数应用') - 事件监听器
function test() { var a = 0; //事件监听器 保持对test作用域的访问 $('ele').on('click', function() { a++; }); } - ajax
! function() { var localData = "localData here"; var url = "http://www.baidu.com/"; //ajax使用了localData,url $.ajax({ url: url, success: function() { // do sth... console.log(localData); } }); }(); - 异步(同步)操作
- 只要使用了回调函数,实际上就是使用了闭包。
- 模块
var foo = ( function Module() { var something = 'cool'; var another = [1, 2]; function doSomething() { console.log(something) } function doAnother() { console.log(another.join(',')) } return { doSomething: doSomething, doAnother: doAnother } } )(); foo.doSomething(); foo.doAnother();
this指针
何为this
- this就是指针, 指向我们调用函数的对象; this是JavaScript中的一个关键字,它是函数运行时,在函数体内自动生成的一个对象,只能在函数体内部使用。
this取值时机(参考文章)
- this取什么值是在函数调用的时候确认的,不是定义的时候确认的 !!!
this的几种赋值情况
-
全局上下文
- 非严格模式和严格模式中this都是指向顶层对象(浏览器中是
window)。this === window // true 'use strict' this === window; this.name = 'wchao'; console.log(this.name); // wchao
- 非严格模式和严格模式中this都是指向顶层对象(浏览器中是
-
函数上下文
-
普通函数调用模式
- 非严格模式
var name = 'window'; var doSth = function(){ console.log(this.name); } doSth(); // 'window' // 因为es5的全局变量挂载到顶层对象(window)上let name = 'window'; let doSth = function(){ console.log(this === window); console.log(this.name); } doSth(); // 'true' 'undefined' // 因为let没有给顶层对象中(浏览器是window)添加属性,this.name在window上找不到 - 严格模式
'use strict' var name = 'window'; var doSth = function(){ console.log(typeof this === 'undefined'); console.log(this.name); } doSth(); // true,// 报错,因为this是undefined - 举例
var name = '轩辕Rowboat'; setTimeout(function(){ console.log(this.name); //this指向window 轩辕Rowboat }, 0);
- 非严格模式
-
对象中的函数(方法)调用模式
var name = 'window'; var doSth = function(){ console.log(this.name); } var student = { name: '轩辕Rowboat', doSth: doSth, other: { name: 'other', doSth: doSth, } } student.doSth(); // '轩辕Rowboat' student.other.doSth(); // 'other' // 用call类比则为: student.doSth.call(student); // '轩辕Rowboat' // 用call类比则为: student.other.doSth.call(student); // 把对象中的函数赋值成一个变量,变回变成普通函数 var studentDoSth = student.doSth; studentDoSth(); // 'window' // 用call类比则为: studentDoSth.call(undefined); -
- 相同
- 三个方法均可以改变普通函数的this指向
- 不同
- 参数不同
fun.call(thisArg, arg1, ... , argN) fun.apply(thisArg, [argsArray]) fun.bind(thisArg, arg1, ... , argN)- call,apply方法之后调用后,函数立即执行。bind方法调用后,返回了一个改变this后的函数,不会立即执行。
- 被bind绑定过this的函数,this不会再被改变
- 常见应用
- 判断数据类型
Object.prototype.toString.call(null) //[object Null] - 获取函数最大值和最小值
Math.max.apply(Math,[1,2,3]) //3 - 伪数组转真数组
Array.prototype.slice.call(arguments)
- 判断数据类型
- 举例
// 有只猫叫小猫,小猫会吃鱼 const cat = { name: '小猫', eatFish(...args) { console.log('cat this指向=>', this); console.log('...args', args); console.log(this.name + '吃鱼'); }, } // 有只狗叫大狗,大狗会吃骨头 const dog = { name: '大狗', eatBone(...args) { console.log('dog this指向=>', this); console.log('...args', args); console.log(this.name + '吃骨头'); }, } console.log('=================== call ========================='); // 有一天大狗想吃鱼了,可是它不知道怎么吃。怎么办?小猫说我吃的时候喂你吃 cat.eatFish.call(dog, '汪汪汪', 'call') // 大狗为了表示感谢,决定下次吃骨头的时候也喂小猫吃 dog.eatBone.call(cat, '喵喵喵', 'call') console.log('=================== apply ========================='); cat.eatFish.apply(dog, ['汪汪汪', 'apply']) dog.eatBone.apply(cat, ['喵喵喵', 'apply']) console.log('=================== bind ========================='); // 有一天他们觉得每次吃的时候再喂太麻烦了。干脆直接教对方怎么吃 const test1 = cat.eatFish.bind(dog, '汪汪汪', 'bind') const test2 = dog.eatBone.bind(cat, '喵喵喵', 'bind') test1() test2()
- 相同
-
构造函数调用模式
function Student(name){ this.name = name; console.log(this); // {name: '轩辕Rowboat'} // 相当于返回了 // return this; } var result = new Student('轩辕Rowboat');- 使用
new操作符调用函数,会自动执行以下步骤:- 创建了一个全新的对象。
- 这个对象会被执行
[[Prototype]](也就是__proto__)链接。 - 生成的新对象会绑定到函数调用的
this。 - 通过
new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上。 - 如果函数没有返回对象类型
Object(包含Functoin,Array,Date,RegExg,Error),那么new表达式中的函数调用会自动返回这个新的对象。
- 变种
function Student(name){ this.name = name; console.log(this); // {name: '轩辕Rowboat'} // 相当于返回了 return function(){ console.log(this) }; } var result = new Student('轩辕Rowboat'); result() // window // 因为result相当于普通函数调用,在顶层调用,this指向window
- 使用
-
原型链中的调用模式
function Student(name){ this.name = name; } var s1 = new Student('轩辕Rowboat'); Student.prototype.doSth = function(){ console.log(this.name); } s1.doSth(); // '轩辕Rowboat'- 此调用模式与对象方法调用模式类似,都会生成新对象,并像原型链上查找
- ES6 class写法
class Student{ constructor(name){ this.name = name; } doSth(){ console.log(this.name); } } let s1 = new Student('轩辕Rowboat'); s1.doSth(); - 箭头函数调用模式
- 箭头函数和普通函数的区别
- 没有自己的
this、super、arguments和new.target绑定。 - 不能使用
new来调用。 - 没有原型对象。
- 不可以改变
this的绑定。 - 形参名称不能重复。
- 没有自己的
- 如果箭头函数被非箭头函数包含,则
this绑定的是最近一层非箭头函数的this,否则this的值则被设置为全局对象。var name = 'window'; var student = { name: '轩辕Rowboat', doSth: function(){ // var self = this; var arrowDoSth = () => { // console.log(self.name); console.log(this.name); } arrowDoSth(); }, arrowDoSth2: () => { console.log(this.name); } } student.doSth(); // '轩辕Rowboat' student.arrowDoSth2(); // 'window'
- 箭头函数和普通函数的区别
DOM事件处理函数调用- 内联事件处理函数调用
-
-
优先级
new调用 >call、apply、bind调用 > 对象上的函数调用 > 普通函数调用。
-
如何判断this指向
- 找到这个函数的直接调用位置
- 判断函数类型
- 如果是箭头函数,则为第一个包裹箭头函数的普通函数的this指向
- 如果不是箭头函数,但是使用了bind,call,apply等改变this的方法,this被重新绑定为
bind、call、apply函数的第一个参数
- 如果是普通函数,并且没有绑定this
- 如果是new的方式调用,this被绑定到实例上
- 如果被调用,谁调用便指向谁
- 如果直接执行,this指向window
- 思维导图
面试题举例(解题思路)
/**
* Question 1
*/
var name = 'window'
var person1 = {
name: 'person1',
show1: function () {
console.log(this.name)
},
show2: () => console.log(this.name),
show3: function () {
return function () {
console.log(this.name)
}
},
show4: function () {
return () => console.log(this.name)
}
}
var person2 = { name: 'person2' }
person1.show1() // person1
person1.show1.call(person2) // person2
person1.show2() // window
person1.show2.call(person2) // window
person1.show3()() //window
person1.show3().call(person2) // person2
person1.show3.call(person2)() // window
person1.show4()() // person1
person1.show4().call(person2) // person1
person1.show4.call(person2)() // person2
/**
* Question 2
*/
var name = 'window'
function Person (name) {
this.name = name;
this.show1 = function () {
console.log(this.name)
}
this.show2 = () => console.log(this.name)
this.show3 = function () {
return function () {
console.log(this.name)
}
}
this.show4 = function () {
return () => console.log(this.name)
}
}
var personA = new Person('personA')
var personB = new Person('personB')
personA.show1() // personA
personA.show1.call(personB) // personB
personA.show2() // personA
personA.show2.call(personB) // personA
personA.show3()() // window
personA.show3().call(personB) // personB
personA.show3.call(personB)() // window
personA.show4()() // personA
personA.show4().call(personB) // personA
personA.show4.call(personB)() // personB
- 手写
bind call apply方法
// bind
Function.prototype.bind = function(){
if (typeof this !== 'function') {
throw new TypeError('Error')
}
//将参数转为数组
const args = Array.prototype.slice.call(arguments)
// 获取this(取出数组第一项,剩余未参数)
const t = args.shift()
const self = this //当前函数
// 返回一个函数
return function(){
// 执行原函数,并返回结果
return self.apply(t,args)
}
}
Function.prototype.bind = function(context,...args){
if (typeof this !== 'function') {
throw new TypeError('Error')
}
const fn = this //获取被调用函数
if(!context) context = window // 没有context,或者传递的是 null undefined,则重置为window
return function(){
return fn.apply(context,[...args])
}
}
// call
Function.prototype.call = function(context,...args){
if (typeof this !== "function") {
throw new TypeError('Error')
}
if(!context) context = window; // 没有context,或者传递的是 null undefined,则重置为window
const fn = Symbol(); // 指定唯一属性,防止 delete 删除错误
context[fn] = this; // 将 this 添加到 context的属性上
const result = context[fn](...args); // 直接调用context 的 fn
delete context[fn]; // 删除掉context新增的symbol属性
return result; // 返回返回值
}
// apply
Function.prototype.apply = function(context, args = []) { // args默认参数为空数组
if(typeof this !== 'function') {
throw new TypeError(`It's must be a function`)
}
if(!context) context = window;
const fn = Symbol();
context[fn] = this;
const result = context[fn](...args); // Function.prototype(...args)
delete context[fn];
return result;
}
- 异步
进程与线程
进程
- 何为进程(浏览器为多进程)
- 进程是 cpu 资源分配的最小单位(是能拥有资源和独立运行的最小单位)
线程
- 何为线程(JS为单线程)
- 线程是 cpu 调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
进程与线程的关系
进程好比公司的一个个部门,每个部门有独立的资源,且相互独立的。线程好比部门里的员工,每个员工协作完成任务,员工之间共享空间。
JS运行机制
JavaScript是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为什么JavaScript不能有多个线程
与它的用途有关,作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器就无法决定采用哪个线程的操作。当然,我们可以为浏览器引入“锁”的机制来解决这些冲突,但这会大大提高复杂性,所以 JavaScript 从诞生开始就选择了单线程执行。
单线程所产生的问题
JavaScript 是单线程的,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着,从而导致阻塞。 居于此问题,将任务分为了以下两种:
- 同步任务(synchronous)
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务; - 异步任务(asynchronous)
异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。 - JS执行异步代码的过程
已AJAX异步请求为例
主线程在发起 AJAX 请求后,会继续执行其他代码。主线程在执行完当前循环中的所有代码后,就会到消息队列取出这条消息,并执行它。到此为止,就完成了工作线程对主线程的通知,回调函数也就得到了执行。如果一开始主线程就没有提供回调函数,AJAX 线程在收到 HTTP 响应后,也就没必要通知主线程,从而也没必要往消息队列放消息。如图
Web Worker
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
event loop(事件循环/事件轮询)
- 什么是event loop
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。 - 图解:
- 主线程运行的时候,产生堆(heap)和 执行栈(call stack)
- 栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)
- 只要执行栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。
- Node.js的Event Loop
- Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。
- Node.js的运行机制
- (1)V8引擎解析JavaScript脚本。
- (2)解析后的代码,调用Node API。
- (3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
- (4)V8引擎再将结果返回给用户。
- Node.js新增两个与"任务队列"有关的方法
- process.nextTick 它指定的任务总是发生在所有异步任务之前
process.nextTick(function A() { console.log(1); process.nextTick(function B(){console.log(2);}); }); setTimeout(function timeout() { console.log('TIMEOUT FIRED'); }, 0) // 1 // 2 // TIMEOUT FIRED- setImmediate setImmediate指定的回调函数,总是排在setTimeout前面
宏任务/微任务
- 宏任务(macroTask)
- 什么是宏任务
可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行) - 哪些属于宏任务
主代码块, setTimeout, setInterval, setImmediate, requestAnimationFrame,Ajax,Dom事件, I/O, UI rendering - 优先级:
主代码块 > setImmediate > MessageChannel > setTimeout / setInterval
- 什么是宏任务
- 微任务(microtask)
- 什么是微任务
可以理解是在当前 task 执行结束后立即执行的任务 - 哪些属于微任务
process.nextTick, Promise, Object.observe, MutationObserver,async/awite - 优先级
process.nextTick > Promise > MutationObserver
- 什么是微任务
- 宏任务与微任务的根本区别
- 宏任务执行流程,执行栈(call stack)-> 尝试DOM渲染 -> Web APIs -> 回调队列(callback Queue)-> 触发eventlop
- 微任务执行流程,执行栈(call stack)-> 微任务队列(micro task Queue) -> 尝试DOM渲染 -> 触发eventlop
- 综上所述:一个在渲染DOM后,一个在渲染DOM前,所以微任务比宏任务触发时机早
- 面试题举例
async function async1(){ console.log('async1 start') await async2() console.log('async1 end') } async function async2(){ console.log('async2') } async function async3(){ console.log('async3') } console.log('sctipt start') setTimeout(function(){ console.log('setTimeout') new Promise(function(resolve){ console.log('promise3') resolve() }).then(function(){ console.log('promise4') }) },0) async1() new Promise(function(resolve){ console.log('promise1') setTimeout(function(){ console.log('setTimeout1') },0) resolve() }).then(function(){ console.log('promise2') }) console.log('sctipt end') //输出结果 // sctipt start // async1 start // async2 // promise1 // sctipt end // async1 end // promise2 // setTimeout // promise3 // promise4 // setTimeout1
promise
什么是promise
Promise是ES6加入标准的一种异步编程解决方案,通常用来表示一个异步操作的最终完成 (或失败)。Promise标准的提出,解决了JavaScript地狱回调的问题。
语法
var p = new Promise(function(resolve, reject) {...} /* executor */);
p.then(() => {}) // 成功resolve
.catch(()=> {}); // 失败reject
promise的状态
- 三种状态
- pending(待定)初始状态
- fulfilled(实现/成功)操作成功
- rejected(被否决)操作失败
- 状态改变
- then 正常返回resolved状态,里面有报错则返回rejected状态
- catch 正常返回resolved状态,里面有报错则返回rejected状态
- 面试题举例
console.log('here we go')
new Promise(resolve => {
setTimeout(()=>{
resolve()
},2000)
}).then(()=>{
console.log('start')
throw new Error('test error1')
}).catch(err1=>{
console.log('I catch:',err1)
}).then(()=>{
console.log('arrice here')
}).then(()=>{
console.log('I arrice here')
throw new Error('test error2')
}).catch((err2)=>{
console.log('No, I cathc:',err2)
throw new Error('test error3')
}).then(()=>{
console.log('...and here')
}).catch((err3)=>{
console.log('NoNoNo, I cathc:',err3)
})
//输出结果
here we go
start
I catch test error1
arrice here
I arrice here
No, I cathc:test error2
NoNoNo, I cathc:another error
promise常用方法
Promise.all(iterable)
这个方法返回一个新的 promise 对象。一般该方法会接受一个iterable参数,里面是一个promise列表,当所有的promise都触发成功时才会触发成功,一旦有一个失败了,则会马上停止其他promise的执行。当iterable里面的结果都执行成功了,这个新的promise对象会将所有的结果已数组的形式依次返回。当有一个失败是,这个新的promise对象会将失败的信息返回。const promise1 = Promise.resolve(3); const promise2 = 42; const promise3 = new Promise((resolve, reject) => { setTimeout(resolve, 100, 'foo'); }); Promise.all([promise1, promise2, promise3]).then((values) => { console.log(values); }); // expected output: Array [3, 42, "foo"]Promise.race(iterable)
方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。const promise1 = new Promise((resolve, reject) => { setTimeout(resolve, 500, 'one'); }); const promise2 = new Promise((resolve, reject) => { setTimeout(resolve, 100, 'two'); }); Promise.race([promise1, promise2]).then((value) => { console.log(value); // Both resolve, but promise2 is faster }); // expected output: "two"Promise.reject(rease)
方法返回一个带有拒绝原因的Promise对象。function resolved(result) { console.log('Resolved'); } function rejected(result) { console.error(result); } Promise.reject(new Error('fail')).then(resolved, rejected); // expected output: Error: failPromise.resolve(value)
方法返回一个以给定值解析后的Promise对象。如果这个值是一个 promise ,那么将返回这个 promise ;如果这个值是thenable(即带有"then"方法),返回的promise会“跟随”这个thenable的对象,采用它的最终状态;否则返回的promise将以此值完成。此函数将类promise对象的多层嵌套展平。const promise1 = Promise.resolve(123); promise1.then((value) => { console.log(value); // expected output: 123 });
async/await
什么是async/await
- 在解释async/await 之前需要前置了解下
Generator生成器- Generator语法
function* gen() { yield 1; yield 2; yield 3; } let g = gen(); // "Generator { }"
- Generator语法
- async/await 简单来说就是
Generator的语法糖,他就像是隧道尽头的亮光,很多人认为它是异步操作的终极解决方案。- async/await语法
async function gen() { await 1; await 2; await 3; }
- async/await语法
- async 函数的优点
- 内置执行器
- 更好的语义
- 更广的实用性
- async/await 与 promise 的关系
- async只会返回一个Promise,即使返回的是值,它也会进行包裹,类似于then的返回。
async function testAsync() { return "hello async"; } const result = testAsync(); console.log(result); //Promise { 'hello async' }- await 相当于 promise的then
- await命令后面的promise有可能是rejecked,一般用try catch 来捕获promise的 catch
async function myFunction() { try { await somethingThatReturnsAPromise(); } catch (err) { console.log(err); } } // 另一种写法 async function myFunction() { await somethingThatReturnsAPromise().catch(function (err){ console.log(err); }); } - await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。
- async/await的优势
- 在于处理 then 链
/** * 传入参数 n,表示这个函数执行的时间(毫秒) * 执行的结果是 n + 200,这个值将用于下一步骤 */ function takeLongTime(n) { return new Promise(resolve => { setTimeout(() => resolve(n + 200), n); }); } function step1(n) { console.log(`step1 with ${n}`); return takeLongTime(n); } function step2(n) { console.log(`step2 with ${n}`); return takeLongTime(n); } function step3(n) { console.log(`step3 with ${n}`); return takeLongTime(n); } // 现在用 Promise 方式来实现这三个步骤的处理 function doIt() { console.time("doIt"); const time1 = 300; step1(time1) .then(time2 => step2(time2)) .then(time3 => step3(time3)) .then(result => { console.log(`result is ${result}`); console.timeEnd("doIt"); }); } doIt(); // c:\var\test>node --harmony_async_await . // step1 with 300 // step2 with 500 // step3 with 700 // result is 900 // doIt: 1507.251ms // 如果用 async/await 来实现呢,会是这样 async function doIt() { console.time("doIt"); const time1 = 300; const time2 = await step1(time1); const time3 = await step2(time2); const result = await step3(time3); console.log(`result is ${result}`); console.timeEnd("doIt"); } doIt();
- 在于处理 then 链
for-of的应用场景
- for...in(以及forEach) 是常规的同步遍历
- 定义:
for...in语句以任意顺序遍历一个对象的除Symbol以外的可枚举属性 - 语法
for (variable in object) statement
- 定义:
- for...of 常用于异步的遍历
- 定义:
for...of语句在可迭代对象(包括Array,Map,Set,String,TypedArray,arguments对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句 - 语法
for (variable of iterable) { //statements }
- 定义:
- 两者区别
function muti(num){
return new Promise(resolve => {
setTimeout(()=>{
resolve(num * num)
},1000)
})
}
const nums = [1,2,3];
nums.forEach(async (i) => {
const res = await muti(i)
console.log(res)
})
nums.forEach(async (i) => {
const res = await muti(i)
console.log(res)
})
// 结果同时执行
!(async function(){
for(let i of nums){
const res = await muti(i)
console.log(res)
}
})()
// 结果异步执行