JS
2020-06-05 14:37:43 4 举报
AI智能生成
前端面试 js
作者其他创作
大纲/内容
JS 基础知识及常考面试题
原始Primitive类型
原始类型有哪几种?
boolean
null
undefined
number
string
symbol
null
undefined
number
string
symbol
null 是对象嘛?
null 不是 对象
另外对于 null 来说,很多人会认为他是个对象类型,其实这是错误的。虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。
对象Object类型
对象类型和原始类型的不同之处?
在 JS 中,除了原始类型那么其他的都是对象类型了。
对象类型和原始类型不同的是,原始类型存储的是值,对象类型存储的是地址(指针)
对象类型和原始类型不同的是,原始类型存储的是值,对象类型存储的是地址(指针)
函数参数是对象会发生什么问题?
解析
首先,函数传参是传递对象指针的副本
到函数内部修改参数的属性这步,我相信大家都知道,当前 p1 的值也被修改了
但是当我们重新为 person 分配了一个对象时就出现了分歧,请看下图
到函数内部修改参数的属性这步,我相信大家都知道,当前 p1 的值也被修改了
但是当我们重新为 person 分配了一个对象时就出现了分歧,请看下图
所以最后 person 拥有了一个新的地址(指针),也就和 p1 没有任何关系了,导致了最终两个变量的值是不相同的。
typeof vs instanceof
typeof 是否能正确判断类型?
不能
typeof 对于原始类型来说,除了 null 都可以显示正确的类型
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object'
typeof 对于对象来说,除了函数都会显示 object,
所以说 typeof 并不能准确判断变量到底是什么类型
所以说 typeof 并不能准确判断变量到底是什么类型
typeof [] // 'object'
typeof {} // 'object'
typeof {} // 'object'
typeof console.log // 'function'
instanceof 能正确判断对象的原理是什么?
内部机制是通过原型链来判断的
对于原始类型来说,你想直接通过 instanceof 来判断类型是不行的
var str = 'hello world'
str instanceof String // false
var str1 = new String('hello world')
str1 instanceof String // true
str instanceof String // false
var str1 = new String('hello world')
str1 instanceof String // true
让 instanceof 判断原始类型
Symbol.hasInstance 其实就是一个能让我们自定义 instanceof 行为的东西,
以上代码等同于 typeof 'hello world' === 'string',所以结果自然是 true 了。
这其实也侧面反映了一个问题, instanceof 也不是百分之百可信的。
以上代码等同于 typeof 'hello world' === 'string',所以结果自然是 true 了。
这其实也侧面反映了一个问题, instanceof 也不是百分之百可信的。
类型转换
在 JS 中类型转换只有三种情况,分别是:
转换为布尔值
转换为数字
转换为字符串
转换为数字
转换为字符串
类型转换表格
注意图中有一个错误,Boolean 转字符串这行结果我指的是 true 转字符串的例子,不是说 Boolean、函数、Symblo 转字符串都是 `true`
转Boolean
在条件判断时,除了 undefined, null, false, NaN, '', 0, -0,
其他所有值都转为 true,包括所有对象。
其他所有值都转为 true,包括所有对象。
对象转原始类型
对象在转换类型的时候,会调用内置的 [[ToPrimitive]] 函数,
对于该函数来说,算法逻辑一般来说如下:
对于该函数来说,算法逻辑一般来说如下:
1、如果已经是原始类型了,那就不需要转换了
2、如果需要转字符串类型就调用 x.toString(),转换为基础类型的话就返回转换的值。
不是字符串类型的话就先调用 valueOf,结果不是基础类型的话再调用 toString
3、调用 x.valueOf(),如果转换为基础类型,就返回转换的值
4、如果都没有返回原始类型,就会报错
2、如果需要转字符串类型就调用 x.toString(),转换为基础类型的话就返回转换的值。
不是字符串类型的话就先调用 valueOf,结果不是基础类型的话再调用 toString
3、调用 x.valueOf(),如果转换为基础类型,就返回转换的值
4、如果都没有返回原始类型,就会报错
当然你也可以重写 Symbol.toPrimitive ,该方法在转原始类型时调用优先级最高。
四则运算符
加法运算符
加法运算符不同于其他几个运算符,
它有以下几个特点:
它有以下几个特点:
1、运算中其中一方为字符串,那么就会把另一方也转换为字符串
2、如果一方不是字符串或者数字,那么会将它转换为数字或者字符串
2、如果一方不是字符串或者数字,那么会将它转换为数字或者字符串
示例1
1 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3"
true + true // 2
4 + [1,2,3] // "41,2,3"
解析
1、对于第一行代码来说,触发特点一,所以将数字 1 转换为字符串,得到结果 '11'
2、对于第二行代码来说,触发特点二,所以将 true 转为数字 1
3、对于第三行代码来说,触发特点二,所以将数组通过 toString 转为字符串 1,2,3,得到结果 41,2,3
2、对于第二行代码来说,触发特点二,所以将 true 转为数字 1
3、对于第三行代码来说,触发特点二,所以将数组通过 toString 转为字符串 1,2,3,得到结果 41,2,3
示例2
'a' + + 'b' // -> "aNaN"
解析
因为 + 'b' 等于 NaN,所以结果为 "aNaN",
你可能也会在一些代码中看到过 + '1' 的形式来快速获取 number 类型。
你可能也会在一些代码中看到过 + '1' 的形式来快速获取 number 类型。
除了加法之外的运算符
对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字
示例
4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN
4 * [] // 0
4 * [1, 2] // NaN
比较运算符
1、如果是对象,就通过 toPrimitive 转换对象
2、如果是字符串,就通过 unicode 字符索引来比较
2、如果是字符串,就通过 unicode 字符索引来比较
示例
解析
在以上代码中,因为 a 是对象,所以会通过 valueOf 转换为原始类型再比较值。
this
面试题
如何正确判断 this?
箭头函数的 this 是什么?
几个函数调用的场景中
this的指向
this的指向
1、对于直接调用 foo 来说,不管 foo 函数被放在了什么地方,this 一定是 window
2、对于 obj.foo() 来说,我们只需要记住,谁调用了函数,谁就是 this,所以在这个场景下 foo 函数中的 this 就是 obj 对象
3、对于 new 的方式来说,this 被永远绑定在了 c 上面,不会被任何方式改变 this
2、对于 obj.foo() 来说,我们只需要记住,谁调用了函数,谁就是 this,所以在这个场景下 foo 函数中的 this 就是 obj 对象
3、对于 new 的方式来说,this 被永远绑定在了 c 上面,不会被任何方式改变 this
箭头函数this
首先箭头函数其实是没有 this 的,箭头函数中的 this 只取决包裹箭头函数的第一个普通函数的 this
示例
在这个例子中,因为包裹箭头函数的第一个普通函数是 a,所以此时的 this 是 window
对箭头函数使用 bind 这类函数是无效的
bind与this
this 取决于第一个参数,如果第一个参数为空,那么就是 window
一个函数进行多次 bind
可以从上述代码中发现,不管我们给函数 bind 几次,fn 中的 this 永远由第一次 bind 决定,所以结果永远是 window
this规则同时出现时的优先级
1、new 的方式优先级最高,
2、接下来是 bind 这些函数,
3、然后是 obj.foo() 这种调用方式,
4、最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。
2、接下来是 bind 这些函数,
3、然后是 obj.foo() 这种调用方式,
4、最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。
单个规则时,this指向流程图
图中的流程只针对于单个规则
== vs ===
面试题:== 和 === 有什么区别?
对于 == 来说,如果对比双方的类型不一样的话,就会进行类型转换
=== 来说就简单多了,就是判断两者类型和值是否相同
对比 x 和 y 是否相同,判断流程
1、首先会判断两者类型是否相同。相同的话就是比大小了
2、类型不相同的话,那么就会进行类型转换
3、会先判断是否在对比 null 和 undefined,是的话就会返回 true
2、类型不相同的话,那么就会进行类型转换
3、会先判断是否在对比 null 和 undefined,是的话就会返回 true
4、判断两者类型是否为 string 和 number,是的话就会将字符串转换为 number
5、判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断
6、判断其中一方是否为 object 且另一方为 string、number 或者 symbol,是的话就会把 object 转为原始类型再进行判断
流程图
这个流程图并没有将所有的情况都列举出来,
我这里只将常用到的情况列举了,
如果你想了解更多的内容可以参考 标准文档
我这里只将常用到的情况列举了,
如果你想了解更多的内容可以参考 标准文档
思考题:看完了上面的步骤,对于 [] == ![] 你是否能正确写出答案呢?
[]==[]
两边都是相同数据类型时,==是直接比较两边数据,结果为false,因为[]属于引用类型,在两个[]分别指向不同的堆内存
[]==![]
过程:[]==![] ----> []==false -----> 0==0 --->//true
闭包
面试题
什么是闭包?
闭包的定义其实很简单:函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。
在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。
循环中使用闭包解决 `var` 定义函数的问题
题目
首先因为 setTimeout 是个异步函数,所以会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6
注意输出的时间间隔,和事件循环结合起来理解
解决办法
使用闭包
在上述代码中,我们首先使用了立即执行函数将 i 传入函数内部,这个时候值就被固定在了参数 j 上面不会改变,当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j,从而达到目的。
使用 setTimeout 的第三个参数
使用 setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传入。
其中7是定时器的id ,
settimeout有返回值,便于清除的时候找到是哪个
settimeout有返回值,便于清除的时候找到是哪个
使用 let 定义 i
深浅拷贝
面试题:什么是浅拷贝?如何实现浅拷贝?什么是深拷贝?如何实现深拷贝?
浅拷贝
Object.assign
Object.assign 只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝
展开运算符...
深拷贝
JSON.parse(JSON.stringify(object))
该方法也是有局限性的
1、会忽略 undefined
2、会忽略 symbol
3、不能序列化函数
4、不能解决循环引用的对象
2、会忽略 symbol
3、不能序列化函数
4、不能解决循环引用的对象
不能解决循环引用的对象
实现深拷贝
MessageChannel
如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用 MessageChannel
手写深拷贝
其实实现一个深拷贝是很困难的,需要我们考虑好多种边界情况,比如原型链如何处理、DOM 如何处理等等,所以这里我们实现的深拷贝只是简易版,并且我其实更推荐使用 lodash 的深拷贝函数。
Reflect.ownKeys(newObj)
可以替换成
Object.keys(newObj)
可以替换成
Object.keys(newObj)
原型
__proto__ 属性
每个 JS 对象都有 __proto__ 属性,这个属性指向了原型
原型也是一个对象,并且这个对象中包含了很多函数,所以我们可以得出一个结论:对于 obj 来说,可以通过 __proto__ 找到一个原型对象,在该对象中定义了很多函数让我们来使用。
constructor 属性,也就是构造函数
打开 constructor 属性我们又可以发现其中还有一个 prototype 属性,并且这个属性对应的值和先前我们在 __proto__ 中看到的一模一样。所以我们又可以得出一个结论:原型的 constructor 属性指向构造函数,构造函数又通过 prototype 属性指回原型,但是并不是所有函数都具有这个属性,Function.prototype.bind() 就没有这个属性
原型和原型链图示
原型链
原型链就是多个对象通过 __proto__ 的方式连接了起来。为什么 obj 可以访问到 valueOf 函数,就是因为 obj 通过原型链找到了
总结
1、Object 是所有对象的爸爸,所有对象都可以通过 __proto__ 找到它
2、Function 是所有函数的爸爸,所有函数都可以通过 __proto__ 找到它
3、函数的 prototype 是一个对象
4、对象的 __proto__ 属性指向原型, __proto__ 将对象和原型连接起来组成了原型链
2、Function 是所有函数的爸爸,所有函数都可以通过 __proto__ 找到它
3、函数的 prototype 是一个对象
4、对象的 __proto__ 属性指向原型, __proto__ 将对象和原型连接起来组成了原型链
ES6 知识点及常考面试题
var、let 及 const 区别
面试题
什么是提升?
什么是暂时性死区?
var、let 及 const 区别?
什么是暂时性死区?
var、let 及 const 区别?
提升
var声明的变量提升
例子1
console.log(a) // undefined
var a = 1
var a = 1
虽然变量还没有被声明,但是我们却可以使用这个未被声明的变量,这种情况就叫做提升,并且提升的是声明。
对于这种情况,我们可以把代码这样来看:
var a
console.log(a) // undefined
a = 1
var a
console.log(a) // undefined
a = 1
例子2
var a = 10
var a
console.log(a) //10
var a
console.log(a) //10
上段代码可以这样来看:
var a
var a
a = 10
console.log(a)
var a
var a
a = 10
console.log(a)
子主题
子主题
使用 var 声明的变量会被提升到作用域的顶部
函数提升
console.log(a) // ƒ a() {}
function a() {}
var a = 1
function a() {}
var a = 1
打印结果会是 ƒ a() {},即使变量声明在函数之后,
这也说明了函数会被提升,并且优先于变量提升
这也说明了函数会被提升,并且优先于变量提升
提升存在的根本原因
为了解决函数间互相调用的情况
假如不存在提升这个情况,那么就实现不了上述的代码,因为不可能存在 test1 在 test2 前面然后 test2 又在 test1 前面。
let const
与var的区别
与var的区别
let b = 1
const c = 1
console.log(window.b) // undefined
console.log(window. c) // undefined
const c = 1
console.log(window.b) // undefined
console.log(window. c) // undefined
首先在全局作用域下使用 let 和 const 声明变量,
变量并不会被挂载到 window 上,这一点就和 var 声明有了区别
变量并不会被挂载到 window 上,这一点就和 var 声明有了区别
暂时性死区
在声明 a 之前如果使用了 a,就会出现报错的情况
首先报错的原因是因为存在暂时性死区,我们不能在声明前就使用变量,这也是 let 和 const 优于 var 的一点。
然后这里你认为的提升和 var 的提升是有区别的,虽然变量在编译的环节中被告知在这块作用域中可以访问,但是访问是受限制的。
然后这里你认为的提升和 var 的提升是有区别的,虽然变量在编译的环节中被告知在这块作用域中可以访问,但是访问是受限制的。
总结
1、函数提升优先于变量提升,函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪到作用域顶部
2、var 存在提升,我们能在声明之前使用。let、const 因为暂时性死区的原因,不能在声明前使用
3、var 在全局作用域下声明变量会导致变量挂载在 window 上,其他两者不会
4、let 和 const 作用基本一致,但是后者声明的变量不能再次赋值
2、var 存在提升,我们能在声明之前使用。let、const 因为暂时性死区的原因,不能在声明前使用
3、var 在全局作用域下声明变量会导致变量挂载在 window 上,其他两者不会
4、let 和 const 作用基本一致,但是后者声明的变量不能再次赋值
原型继承和 Class 继承
面试题
原型如何实现继承?
Class 如何实现继承?
Class 本质是什么?
Class 如何实现继承?
Class 本质是什么?
class
其实在 JS 中并不存在类,class 只是语法糖,本质还是函数。
class Person {}
Person instanceof Function // true
Person instanceof Function // true
组合继承
组合继承是最常用的继承方式
示例
以上继承的方式核心
1、在子类的构造函数中通过 Parent.call(this) 继承父类的属性
2、然后改变子类的原型为 new Parent() 来继承父类的函数
组合继承优缺点
优点
构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数,
普通函数使用了new,他本体也算构造函数
构造函数指的是Child
构造函数指的是Child
缺点
但是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,
导致子类的原型上多了不需要的父类属性,存在内存上的浪费。
导致子类的原型上多了不需要的父类属性,存在内存上的浪费。
构造函数指的是new的时候调用的constructor
寄生组合继承
这种继承方式对组合继承进行了优化,
组合继承缺点在于继承父类函数时调用了构造函数,我们只需要优化掉这点就行了。
组合继承缺点在于继承父类函数时调用了构造函数,我们只需要优化掉这点就行了。
示例
Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
组合寄生继承实现的核心
将父类的原型赋值给了子类,并且将构造函数设置为子类,
这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数
这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数
Class 继承
class 实现继承的核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super,因为这段代码可以看成 Parent.call(this, value)
模块化
面试题
为什么要使用模块化?
都有哪几种方式可以实现模块化,各有什么特点?
都有哪几种方式可以实现模块化,各有什么特点?
模块化带来的好处
1、解决命名冲突
2、提供复用性
3、提高代码可维护性
2、提供复用性
3、提高代码可维护性
立即执行函数
在早期,使用立即执行函数实现模块化是常见的手段,通过函数作用域解决了命名冲突、污染全局作用域的问题
AMD 和 CMD
鉴于目前这两种实现方式已经很少见到,所以不再对具体特性细聊,只需要了解这两者是如何使用的。
CommonJS
CommonJS 最早是 Node 在使用,目前也仍然广泛使用,比如在 Webpack 中你就能见到它,当然目前在 Node 中的模块管理已经和 CommonJS 有一些区别了
ES Module
ES Module 是原生实现的模块化方案
ES Module与 CommonJS 有以下几个区别
1、CommonJS 支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
2、CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
3、CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
4、ES Module 会编译成 require/exports 来执行的
2、CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
3、CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
4、ES Module 会编译成 require/exports 来执行的
// 引入模块 API
import XXX from './a.js'
import { XXX } from './a.js'
// 导出模块 API
export function a() {}
export default function() {}
import XXX from './a.js'
import { XXX } from './a.js'
// 导出模块 API
export function a() {}
export default function() {}
Proxy
面试题
Proxy 可以实现什么功能?
Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。
let p = new Proxy(target, handler)
target 代表需要添加代理的对象,handler 用来自定义对象中的操作,比如可以用来自定义 set 或者 get 函数。
let p = new Proxy(target, handler)
target 代表需要添加代理的对象,handler 用来自定义对象中的操作,比如可以用来自定义 set 或者 get 函数。
Proxy 实现一个简易版数据响应式
我们通过自定义 set 和 get 函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。
vue 3.0中的Proxy
Vue3.0 中将会通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式
果需要实现一个 Vue 中的响应式,需要我们在 get 中收集依赖,在 set 派发更新,
之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,
并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,
唯一缺陷可能就是浏览器的兼容性不好了。
之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,
并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,
唯一缺陷可能就是浏览器的兼容性不好了。
Proxy 无需一层层递归为每个属性添加代理
map, filter, reduce
面试题:
map, filter, reduce 各自有什么作用?
map
map 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后放入到新的数组中。
[1, 2, 3].map(v => v + 1) // -> [2, 3, 4]
[1, 2, 3].map(v => v + 1) // -> [2, 3, 4]
另外 map 的回调函数接受三个参数,分别是当前索引元素,索引,原数组
['1','2','3'].map(parseInt)
['1','2','3'].map(parseInt)
第一轮遍历 parseInt('1', 0) -> 1
第二轮遍历 parseInt('2', 1) -> NaN
第三轮遍历 parseInt('3',
第二轮遍历 parseInt('2', 1) -> NaN
第三轮遍历 parseInt('3',
filter
filter 的作用也是生成一个新数组,在遍历数组的时候将返回值为 true 的元素放入新数组,
我们可以利用这个函数删除一些不需要的元素
我们可以利用这个函数删除一些不需要的元素
let array = [1, 2, 4, 6]
let newArray = array.filter(item => item !== 6)
console.log(newArray) // [1, 2, 4]
let newArray = array.filter(item => item !== 6)
console.log(newArray) // [1, 2, 4]
和 map 一样,filter 的回调函数也接受三个参数,用处也相同。
reduce
reduce 可以将数组中的元素通过回调函数最终转换为一个值。
将函数里的元素全部相加得到一个值
const arr = [1, 2, 3]
const sum = arr.reduce((acc, current) => acc + current, 0)
console.log(sum)
const sum = arr.reduce((acc, current) => acc + current, 0)
console.log(sum)
参数和累加过程
对于 reduce 来说,它接受两个参数,分别是回调函数和初始值,接下来我们来分解上述代码中 reduce 的过程
首先初始值为 0,该值会在执行第一次回调函数时作为第一个参数传入
回调函数接受四个参数,分别为累计值、当前元素、当前索引、原数组,后三者想必大家都可以明白作用,这里着重分析第一个参数
在一次执行回调函数时,当前值和初始值相加得出结果 1,该结果会在第二次执行回调函数时当做第一个参数传入
所以在第二次执行回调函数时,相加的值就分别是 1 和 2,以此类推,循环结束后得到结果 6
首先初始值为 0,该值会在执行第一次回调函数时作为第一个参数传入
回调函数接受四个参数,分别为累计值、当前元素、当前索引、原数组,后三者想必大家都可以明白作用,这里着重分析第一个参数
在一次执行回调函数时,当前值和初始值相加得出结果 1,该结果会在第二次执行回调函数时当做第一个参数传入
所以在第二次执行回调函数时,相加的值就分别是 1 和 2,以此类推,循环结束后得到结果 6
reduce 来实现 map 函数
JS 异步编程及常考面试题
并发(concurrency)和并行(parallelism)区别
面试题:
并发与并行的区别?
区别
并发是宏观概念,我分别有任务 A 和任务 B,在一段时间内通过任务间的切换完成了这两个任务,这种情况就可以称之为并发。
并行是微观概念,假设 CPU 中存在两个核心,那么我就可以同时完成任务 A、B。同时完成多个任务的情况就可以称之为并行。
并行是微观概念,假设 CPU 中存在两个核心,那么我就可以同时完成任务 A、B。同时完成多个任务的情况就可以称之为并行。
回调函数(Callback)
面试题:
什么是回调函数?
回调函数有什么缺点?
如何解决回调地狱问题?
回调函数有什么缺点?
如何解决回调地狱问题?
一个回调函数的例子
ajax(url, () => {
// 处理逻辑
})
// 处理逻辑
})
回调地狱
回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)
请求存在依赖性,可能就会写出如下代码:
回调地狱的根本问题就是:
1、嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身
2、嵌套函数一多,就很难处理错误
2、嵌套函数一多,就很难处理错误
回调函数中存在的其他问题
比如不能使用 try catch 捕获错误,
不能直接 return。
不能直接 return。
Generator
面试题:
你理解的 Generator 是什么?
1、首先 Generator 函数调用和普通函数不同,它会返回一个迭代器
2、当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6
3、当执行第二次 next 时,传入的参数等于上一个 yield 的返回值,如果你不传参,yield 永远返回 undefined。此时 let y = 2 * 12,所以第二个 yield 等于 2 * 12 / 3 = 8
4、当执行第三次 next 时,传入的参数会传递给 z,所以 z = 13, x = 5, y = 24,相加等于 42
2、当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6
3、当执行第二次 next 时,传入的参数等于上一个 yield 的返回值,如果你不传参,yield 永远返回 undefined。此时 let y = 2 * 12,所以第二个 yield 等于 2 * 12 / 3 = 8
4、当执行第三次 next 时,传入的参数会传递给 z,所以 z = 13, x = 5, y = 24,相加等于 42
通过 Generator 函数解决回调地狱的问题
Promise
面试题
Promise 的特点是什么,分别有什么优缺点?
什么是 Promise 链?
Promise 构造函数执行和 then 函数执行有什么区别?
什么是 Promise 链?
Promise 构造函数执行和 then 函数执行有什么区别?
是什么
Promise 翻译过来就是承诺的意思,这个承诺会在未来有一个确切的答复
三种状态
1、等待中(pending)
2、完成了 (resolved)
3、拒绝了(rejected)
2、完成了 (resolved)
3、拒绝了(rejected)
状态不可逆
这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了,也就是说一旦状态变为 resolved 后,就不能再次改变
当我们在构造 Promise 的时候,构造函数内部的代码是立即执行的
链式调用
Promise 实现了链式调用,也就是说每次调用 then 之后返回的都是一个 Promise,并且是一个全新的 Promise,原因也是因为状态不可变。
如果你在 then 中 使用了 return,那么 return 的值会被 Promise.resolve() 包装
如果你在 then 中 使用了 return,那么 return 的值会被 Promise.resolve() 包装
解决回调地狱
缺点
如无法取消 Promise,
错误需要通过回调函数捕获
错误需要通过回调函数捕获
async 及 await
面试题
async 及 await 的特点,
它们的优点和缺点分别是什么?
await 原理是什么?
它们的优点和缺点分别是什么?
await 原理是什么?
一个函数如果加上 async ,那么该函数就会返回一个 Promise
async 就是将函数返回值使用 Promise.resolve() 包裹了下,和 then 中处理返回值一样,
并且 await 只能配套 async 使用
并且 await 只能配套 async 使用
async function test() {
let value = await sleep()
}
let value = await sleep()
}
async 和 await 是异步终极解决方案
async 和 await 可以说是异步终极解决方案了,
相比直接使用 Promise 来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码,毕竟写一大堆 then 也很恶心,并且也能优雅地解决回调地狱问题。
相比直接使用 Promise 来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码,毕竟写一大堆 then 也很恶心,并且也能优雅地解决回调地狱问题。
缺点
因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。
await例子
1、首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为 await 内部实现了 generator ,generator 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
2、因为 await 是异步操作,后来的表达式不返回 Promise 的话,就会包装成 Promise.reslove(返回值),然后会去执行函数外的同步代码
3、同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 0 + 10
2、因为 await 是异步操作,后来的表达式不返回 Promise 的话,就会包装成 Promise.reslove(返回值),然后会去执行函数外的同步代码
3、同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 0 + 10
await本质
await 内部实现了 generator,其实 await 就是 generator 加上 Promise 的语法糖,且内部实现了自动执行 generator
常用定时器函数
面试题:
setTimeout、setInterval、requestAnimationFrame 各有什么特点?
常见定时器函数
setTimeout
setInterval
requestAnimationFrame
setInterval
requestAnimationFrame
setTimeout
很多人认为 setTimeout 是延时多久,那就应该是多久后执行。
其实这个观点是错误的,因为 JS 是单线程执行的,如果前面的代码影响了性能,就会导致 setTimeout 不会按期执行。
其实这个观点是错误的,因为 JS 是单线程执行的,如果前面的代码影响了性能,就会导致 setTimeout 不会按期执行。
setInterval
通常来说不建议使用 setInterval
第一,它和 setTimeout 一样,不能保证在预期的时间执行任务
第二,它存在执行累积的问题,请看以下伪代码
以上代码在浏览器环境中,如果定时器执行过程中出现了耗时操作,多个回调函数会在耗时操作结束以后同时执行,这样可能就会带来性能上的问题。
requestAnimationFrame
首先 requestAnimationFrame 自带函数节流功能,基本可以保证在 16.6 毫秒内只执行一次(不掉帧的情况下),
并且该函数的延时效果是精确的,没有其他定时器时间不准的问题,当然你也可以通过该函数来实现 setTimeout。
并且该函数的延时效果是精确的,没有其他定时器时间不准的问题,当然你也可以通过该函数来实现 setTimeout。
EventLoop
进程与线程
面试题:
进程与线程区别?JS 单线程带来的好处?
线程和进程
两个名词都是 CPU 工作时间片的一个描述
进程描述了 CPU 在运行指令及加载和保存上下文所需的时间,放在应用上来说就代表了一个程序。
线程是进程中的更小单位,描述了执行一段指令所需的时间。
线程是进程中的更小单位,描述了执行一段指令所需的时间。
把这些概念拿到浏览器中来说,
当你打开一个 Tab 页时,其实就是创建了一个进程,一个进程中可以有多个线程,比如渲染线程、JS 引擎线程、HTTP 请求线程等等。
当你发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁。
当你打开一个 Tab 页时,其实就是创建了一个进程,一个进程中可以有多个线程,比如渲染线程、JS 引擎线程、HTTP 请求线程等等。
当你发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁。
JS 是单线程
JS 引擎线程和渲染线程,大家应该都知道,在 JS 运行的时候可能会阻止 UI 渲染,这说明了两个线程是互斥的。
这其中的原因是因为 JS 可以修改 DOM,如果在 JS 执行的时候 UI 线程还在工作,就可能导致不能安全的渲染 UI。
这其中的原因是因为 JS 可以修改 DOM,如果在 JS 执行的时候 UI 线程还在工作,就可能导致不能安全的渲染 UI。
执行栈
面试题
什么是执行栈?
执行栈
执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则
当开始执行 JS 代码时,首先会执行一个 main 函数,然后执行我们的代码。根据先进后出的原则,后执行的函数会先弹出栈。
平时在开发中,大家也可以在报错中找到执行栈的痕迹
在上图清晰的看到报错在 foo 函数,
foo 函数又是在 bar 函数中调用的
foo 函数又是在 bar 函数中调用的
爆栈
当我们使用递归的时候,因为栈可存放的函数是有限制的,一旦存放了过多的函数且没有得到释放的话,就会出现爆栈的问题
浏览器中的 Event Loop
面试题
异步代码执行顺序?
解释一下什么是 Event Loop ?
解释一下什么是 Event Loop ?
异步
当我们执行 JS 代码的时候其实就是往执行栈中放入函数,那么遇到异步代码的时候该怎么办?
其实当遇到异步的代码时,会被挂起并在需要执行的时候加入到 Task(有多种 Task) 队列中。
一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为。
其实当遇到异步的代码时,会被挂起并在需要执行的时候加入到 Task(有多种 Task) 队列中。
一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为。
任务源
不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。
在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task。
下面来看以下代码的执行顺序:
在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task。
下面来看以下代码的执行顺序:
例子
解释
首先先来解释下上述代码的 async 和 await 的执行顺序。当我们调用 async1 函数时,会马上输出 async2 end,并且函数返回一个 Promise,接下来在遇到 await的时候会就让出线程开始执行 async1 外的代码,所以我们完全可以把 await 看成是让出线程的标志。
然后当同步代码全部执行完毕以后,就会去执行所有的异步代码,那么又会回到 await 的位置执行返回的 Promise 的 resolve 函数,这又会把 resolve 丢到微任务队列中,接下来去执行 then 中的回调,当两个 then 中的回调全部执行完毕以后,又会回到 await 的位置处理返回值,这时候你可以看成是 Promise.resolve(返回值).then(),然后 await 后的代码全部被包裹进了 then 的回调中,所以 console.log('async1 end') 会优先执行于 setTimeout。
然后当同步代码全部执行完毕以后,就会去执行所有的异步代码,那么又会回到 await 的位置执行返回的 Promise 的 resolve 函数,这又会把 resolve 丢到微任务队列中,接下来去执行 then 中的回调,当两个 then 中的回调全部执行完毕以后,又会回到 await 的位置处理返回值,这时候你可以看成是 Promise.resolve(返回值).then(),然后 await 后的代码全部被包裹进了 then 的回调中,所以 console.log('async1 end') 会优先执行于 setTimeout。
把 async 的这两个函数改造成你一定能理解的代码
Event Loop 执行顺序如下所示:
1、首先执行同步代码,这属于宏任务
2、当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行
3、执行所有微任务
4、当执行完所有微任务后,如有必要会渲染页面
5、然后开始下一轮 Event Loop,执行宏任务中的异步代码,也就是 setTimeout 中的回调函数
2、当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行
3、执行所有微任务
4、当执行完所有微任务后,如有必要会渲染页面
5、然后开始下一轮 Event Loop,执行宏任务中的异步代码,也就是 setTimeout 中的回调函数
宏任务和微任务有哪些
微任务包括 process.nextTick ,promise ,MutationObserver,其中 process.nextTick 为 Node 独有。
宏任务包括 script , setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering
宏任务包括 script , setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering
一个错误认识
这里很多人会有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话才会先执行微任务。
Node 中的 Event Loop
略过没看
面试题
Node 中的 Event Loop 和浏览器中的有什么区别?
process.nexttick 执行顺序?
process.nexttick 执行顺序?
Node 中的 Event Loop 和浏览器中的是完全不相同的东西。
Node 的 Event Loop 分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。
Node 的 Event Loop 分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。
JS 进阶知识点及常考面试题
手写 call、apply 及 bind 函数
面试题
call apply 及 bind 函数内部实现是怎么样的?
apply 参数是一个数组,call bind参数是一个序列
call
分析
1、首先 context 为可选参数,如果不传的话默认上下文为 window
2、接下来给 context 创建一个 fn 属性,并将值设置为需要调用的函数
3、因为 call 可以传入多个参数作为调用函数的参数,所以需要将参数剥离出来
4、然后调用函数并将对象上的函数删除
2、接下来给 context 创建一个 fn 属性,并将值设置为需要调用的函数
3、因为 call 可以传入多个参数作为调用函数的参数,所以需要将参数剥离出来
4、然后调用函数并将对象上的函数删除
apply
bind
bind 的实现对比其他两个函数略微地复杂了一点,
因为 bind 需要返回一个函数,需要判断一些边界问题,以下是 bind 的实现
因为 bind 需要返回一个函数,需要判断一些边界问题,以下是 bind 的实现
分析
1、前几步和之前的实现差不多,就不赘述了
2、bind 返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过 new 的方式,我们先来说直接调用的方式
3、对于直接调用来说,这里选择了 apply 的方式实现,但是对于参数需要注意以下情况:因为 bind 可以实现类似这样的代码 f.bind(obj, 1)(2),所以我们需要将两边的参数拼接起来,于是就有了这样的实现 args.concat(...arguments)
4、最后来说通过 new 的方式,在之前的章节中我们学习过如何判断 this,对于 new 的情况来说,不会被任何方式改变 this,所以对于这种情况我们需要忽略传入的 this
2、bind 返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过 new 的方式,我们先来说直接调用的方式
3、对于直接调用来说,这里选择了 apply 的方式实现,但是对于参数需要注意以下情况:因为 bind 可以实现类似这样的代码 f.bind(obj, 1)(2),所以我们需要将两边的参数拼接起来,于是就有了这样的实现 args.concat(...arguments)
4、最后来说通过 new 的方式,在之前的章节中我们学习过如何判断 this,对于 new 的情况来说,不会被任何方式改变 this,所以对于这种情况我们需要忽略传入的 this
new
面试题
new 的原理是什么?
通过 new 的方式创建对象和通过字面量创建有什么区别?
通过 new 的方式创建对象和通过字面量创建有什么区别?
在调用 new 的过程中会发生以下四件事情:
1、新生成了一个对象
2、链接到原型
3、绑定 this
4、返回新对象
2、链接到原型
3、绑定 this
4、返回新对象
自己实现一个 new
手写new
分析
1、创建一个空对象
2、获取构造函数
3、设置空对象的原型
4、绑定 this 并执行构造函数
5、确保返回值为对象
2、获取构造函数
3、设置空对象的原型
4、绑定 this 并执行构造函数
5、确保返回值为对象
举个🌰
补充一个关于new的文章
JS 之父的关怀
JS 之父创建了 new 关键字,可以让我们少写几行代码:
JS 之父创建了 new 关键字,可以让我们少写几行代码:
只要你在士兵前面使用 new 关键字,那么可以少做四件事情:
不用创建临时对象,因为 new 会帮你做(你使用「this」就可以访问到临时对象);
不用绑定原型,因为 new 会帮你做(new 为了知道原型在哪,所以指定原型的名字为 prototype);
不用 return 临时对象,因为 new 会帮你做;
不要给原型想名字了,因为 new 指定名字为 prototype。
不用创建临时对象,因为 new 会帮你做(你使用「this」就可以访问到临时对象);
不用绑定原型,因为 new 会帮你做(new 为了知道原型在哪,所以指定原型的名字为 prototype);
不用 return 临时对象,因为 new 会帮你做;
不要给原型想名字了,因为 new 指定名字为 prototype。
这一次我们用 new 来写
new和字面量
对于对象来说,其实都是通过 new 产生的,无论是 function Foo() 还是 let a = { b : 1 }
对于创建一个对象来说,更推荐使用字面量的方式创建对象(无论性能上还是可读性)。因为你使用 new Object() 的方式创建对象需要通过作用域链一层层找到 Object,但是你使用字面量的方式就没这个问题。
function Foo() {}
// function 就是个语法糖
// 内部等同于 new Function()
let a = { b: 1 }
// 这个字面量内部也是使用了 new Object()
// function 就是个语法糖
// 内部等同于 new Function()
let a = { b: 1 }
// 这个字面量内部也是使用了 new Object()
instanceof 的原理
面试题
instanceof 的原理是什么?
原理
instanceof 可以正确的判断对象的类型,
因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype
因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype
实现instanceof
分析
1、首先获取类型的原型
2、然后获得对象的原型
3、然后一直循环判断对象的原型是否等于类型的原型,直到对象原型为 null,因为原型链最终为 null
2、然后获得对象的原型
3、然后一直循环判断对象的原型是否等于类型的原型,直到对象原型为 null,因为原型链最终为 null
为什么 0.1 + 0.2 != 0.3
为什么 0.1 + 0.2 != 0.3?
如何解决这个问题?
如何解决这个问题?
0.1 在二进制中是无限循环的一些数字,其实不只是 0.1,其实很多十进制小数用二进制表示都是无限循环的。
这样其实没什么问题,但是 JS 采用的浮点数标准却会裁剪掉我们的数字。
这样其实没什么问题,但是 JS 采用的浮点数标准却会裁剪掉我们的数字。
怎么解决
用原生提供的方式来最简单的解决问题
parseFloat((0.1 + 0.2).toFixed(10)) === 0.3 // true
垃圾回收机制
面试题
V8 下的垃圾回收机制是怎么样的?
V8 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制。因此,V8 将内存(堆)分为新生代和老生代两部分。
新生代算法
略
老生代算法
略
0 条评论
下一页