JavaScript
# 变量和类型
# js 的数据类型有哪些
js基本类型数据都是直接按值存储在栈中的,包括 Undefined、Null、不是 new 出来的布尔、数字和字符串 。
js引用类型数据被存储于堆中, 如对象、数组、函数 。准确说,是引用类型数据的地址在栈中,但它指向的值在堆中。
此外,Symbol 和 BigInt 是 ES6 中新增的数据类型。
# 如何检测数据类型?
不同类型 | typeof | instanceof | constructor | Object.prototype.toString.call |
---|---|---|---|---|
优点 | 使用简单 | 能检测引用类型 | 基本能检测所有类型(null 和 undefined 除外) | 能检测所有类型 |
缺点 | 只能检测基本类型 | 不能检测基本类型,且不能跨 iframe | constructor 易被修改,不能跨 iframe | ie6 下,undefined 和 null 均为 Object |
typeof
,可以用来判断基本数据类型(除了null
),但是对于判断引用类型就有点不适用,因为它基本都只会返回'object'
,无法判断具体的类型。有些例外,例 如typeof null === 'object'
、typeof function a(){} === 'function'
,typeof NaN === 'number'
,未声明的变量,typeof
照样返回'undefined'
instanceof
,判断对象和构造函数在原型链上是否有关系,即是否在同一个原型链上。无法检测基本数据类型。constructor
,查看对象对应的构造函数。constructor
在其对应对象的原型下面,是自动生成的。当我们写一个构造函数的时候,程序会自动添加:构造函数名.prototype.constructor = 构造函数名
。但是null
和undefined
无法判断,因为是无效的对象。使用 constructor 是不保险的,因为 constructor 属性是可以被修改的,会导致检测出的结果不正确
。
var str = 'hello' alert(str.constructor == String) //true var bool = true alert(bool.constructor == Boolean) //true var num = 123 alert(num.constructor == Number) //true // var nul = null; // alert(nul.constructor == Object);//报错 // var und = undefined; // alert(und.constructor == Object);//报错 var oDate = new Date() alert(oDate.constructor == Date) //true var json = {} alert(json.constructor == Object) //true var arr = [] alert(arr.constructor == Array) //true var reg = /a/ alert(reg.constructor == RegExp) //true var fun = function() {} alert(fun.constructor == Function) //true var error = new Error() alert(error.constructor == Error) //true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22Object.prototype.toString
,返回toString
运行时this
指向的对象类型,返回的类型格式为[object xxx]
,xxx
是具体的数据类型。基本上所有对象的类型都可以通过 这个方法得到。var str = 'hello' console.log(Object.prototype.toString.call(str)) //[object String] var bool = true console.log(Object.prototype.toString.call(bool)) //[object Boolean] var num = 123 console.log(Object.prototype.toString.call(num)) //[object Number] var nul = null console.log(Object.prototype.toString.call(nul)) //[object Null] var und = undefined console.log(Object.prototype.toString.call(und)) //[object Undefined] var oDate = new Date() console.log(Object.prototype.toString.call(oDate)) //[object Date] var json = {} console.log(Object.prototype.toString.call(json)) //[object Object] var arr = [] console.log(Object.prototype.toString.call(arr)) //[object Array] var reg = /a/ console.log(Object.prototype.toString.call(reg)) //[object RegExp] var fun = function() {} console.log(Object.prototype.toString.call(fun)) //[object Function] var error = new Error() console.log(Object.prototype.toString.call(error)) //[object Error]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 为什么 0.1+0.2!==0.3?
出现小数精度丢失的原因:计算机是通过二进制的方式存储数据的,所以计算机计算 0.1+0.2 的时候,实际上是计算的两个数的二进制的和。0.1 的二进制
是0.0001100110011001100...
(1100 循环),0.2 的二进制是:0.00110011001100...
(1100 循环),这两个数的二进制都是无限循环的数。
在 JavaScript 中只有一种数字类型:Number,它的实现遵循 IEEE 754 标准,使用 64 位固定长度来表示,也就是标准的 double 双精度浮点数。在二进制科学表示法中,双精度浮 点数的小数部分最多只能保留 52 位,再加上前面的 1,其实就是保留 53 位有效数字,剩余的需要舍去,遵从“0 舍 1 入”的原则。
根据这个原则,0.1 和 0.2 的二进制数相加,再转化为十进制数就是:0.30000000000000004
。此时就出现了精度丢失。而大数字(大于 2^53)也有精度问题。
避免精度丢失的方法:
- 使用第三方类库例如 math.js 或 decimal.js 等
- 先放大成整数,再运算,最后缩小成小数。
扩展:对于大数字的运算呢?转为字符串运算。
# js 基础
# 浏览器是如何加载 DOM 的?
# JavaScript 脚本延迟加载的方式有哪些?区别是什么?
延迟加载就是等页面加载完成之后再加载 JavaScript 文件。 js 延迟加载有助于提高页面加载速度。
一般有以下几种方式:
defer 属性: 给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻 塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。
async 属性: 给 js 脚本添加 async 属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完 成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。
动态创建 DOM 方式: 动态创建 DOM 标签的方式,可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。
使用 setTimeout 延迟方法: 设置一个定时器来延迟加载 js 脚本文件
让 JS 最后加载: 将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。
# 列举下你知道的数组的原生方法?
- 数组和字符串的转换方法:toString()、toLocalString()、join() 其中 join() 方法可以指定转换为字符串时的分隔符。
- 数组尾部操作的方法 pop() 和 push(),push 方法可以传入多个参数。
- 数组首部操作的方法 shift() 和 unshift() 重排序的方法 reverse() 和 sort(),sort() 方法可以传入一个函数来进行比较,传入前后两个值,如果返回值为正数,则 交换两个参数的位置。
- 数组连接的方法 concat() ,返回的是拼接好的数组,不影响原数组。
- 数组截取办法 slice(),用于截取数组中的一部分返回,不影响原数组。
- 数组插入方法 splice(),影响原数组查找特定项的索引的方法,indexOf() 和 lastIndexOf() 迭代方法 every()、some()、filter()、map() 和 forEach() 方法
- 数组归并方法 reduce() 和 reduceRight() 方法
# 如何遍历伪数组?
arguments
是一个对象,它的属性是从 0 开始依次递增的数字,还有callee
和length
等属性,与数组相似;但是它却没有数组常见的方法属性,如forEach
, reduce
等,所
以叫它们类数组。
要转化类数组,有三个方法:
(1)将数组的方法应用到类数组上,这时候就可以使用call
和apply
方法,如:
function foo() {
Array.prototype.forEach.call(arguments, (a) => console.log(a))
}
2
3
(2)使用 Array.from 方法将类数组转化成数组:
function foo() {
const arrArgs = Array.from(arguments)
arrArgs.forEach((a) => console.log(a))
}
2
3
4
(3)使用展开运算符将类数组转化成数组
function foo() {
const arrArgs = [...arguments]
arrArgs.forEach((a) => console.log(a))
}
2
3
4
还可以直接将 arguments 使用扩展运算符展开
function foo(...args) {
for (let item of args) {
console.log(item)
}
}
2
3
4
5
甚至不需要额外处理,直接使用 for...of 遍历
function foo() {
for (let item of arguments) {
console.log(item)
}
}
2
3
4
5
# 讲下实现一个 AJAX 请求的步骤?
AJAX 是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网 页。
创建 AJAX 请求的步骤:
创建一个 XMLHttpRequest 对象。
在这个对象上使用 open 方法创建一个 HTTP 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发 onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面 进行更新了。
当对象的属性和监听函数设置完成后,最后调用 send 方法来向服务器发起请求,可以传入参数作为发送的数据体。
const SERVER_URL = '/server'
let xhr = new XMLHttpRequest()
// 创建 Http 请求
xhr.open('GET', url, true)
// 设置状态监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return
// 当请求成功时
if (this.status === 200) {
handle(this.response)
} else {
console.error(this.statusText)
}
}
// 设置请求失败时的监听函数
xhr.onerror = function() {
console.error(this.statusText)
}
// 设置请求头信息
xhr.responseType = 'json'
xhr.setRequestHeader('Accept', 'application/json')
// 发送 Http 请求
xhr.send(null)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 对闭包的理解
闭包是指引用另一个函数作用域中变量的函数 (opens new window),创建闭包的最常见的方式就是在一个函数内创建另一个函数,创 建的函数可以访问到当前函数的局部变量。
闭包有两个常用的用途;
闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私 有变量。
闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
比如,函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。
function A() {
let a = 1
window.B = function() {
console.log(a)
}
}
A()
B() // 1
2
3
4
5
6
7
8
9
在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。经典面试题:循环中使用闭包解决 var 定义函数的问题
for (var i = 1; i <= 5; i++) {
setTimeout(() => {
console.log(i)
}, i * 1000)
}
2
3
4
5
首先因为 setTimeout
是个异步函数,所以会先把循环全部执行完毕,这时候 i
就是 6 了,所以会输出一堆 6。解决办法有三种:
- 第一种是使用闭包的方式
for (var i = 1; i <= 5; i++) {
;(function(j) {
setTimeout(() => {
console.log(j)
}, j * 1000)
})(i)
}
2
3
4
5
6
7
在上述代码中,首先使用了立即执行函数将 i
传入函数内部,这个时候值就被固定在了参数 j
上面不会改变,当下次执行 timer
这个闭包的时候,就可以使用外部函数的变
量 j
,从而达到目的。
- 第二种就是使用
setTimeout
的第三个参数,这个参数会被当成timer
函数的参数传入。
for (var i = 1; i <= 5; i++) {
setTimeout(
(j) => {
console.log(j)
},
i * 1000,
i
)
}
2
3
4
5
6
7
8
9
- 第三种就是使用
let
定义i
了来解决问题了,这个也是最为推荐的方式
for (let i = 1; i <= 5; i++) {
setTimeout(() => {
console.log(i)
}, i * 1000)
}
2
3
4
5
# 对 this 对象的理解
this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。
第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。
第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。
第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。
第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数 组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。
这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。
# call、apply 及 bind 函数的区别及实现方式
(1)call 函数的实现步骤:
判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
判断传入上下文对象是否存在,如果不存在,则设置为 window 。
处理传入的参数,截取第一个参数后的所有参数。
将函数作为上下文对象的一个属性。
使用上下文对象来调用这个方法,并保存返回结果。
删除刚才新增的属性。
返回结果。
Function.prototype.myCall = function(context) {
// 判断调用对象
if (typeof this !== 'function') {
console.error('type error')
}
// 获取参数
let args = [...arguments].slice(1),
result = null
// 判断 context 是否传入,如果未传入则设置为 window
context = context || window
// 将调用函数设为对象的方法
context.fn = this
// 调用函数
result = context.fn(...args)
// 将属性删除
delete context.fn
return result
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(2)apply 函数的实现步骤:
判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
判断传入上下文对象是否存在,如果不存在,则设置为 window 。
将函数作为上下文对象的一个属性。
判断参数值是否传入
使用上下文对象来调用这个方法,并保存返回结果。
删除刚才新增的属性
返回结果
Function.prototype.myApply = function(context) {
// 判断调用对象是否为函数
if (typeof this !== 'function') {
throw new TypeError('Error')
}
let result = null
// 判断 context 是否存在,如果未传入则为 window
context = context || window
// 将函数设为对象的方法
context.fn = this
// 调用方法
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
// 将属性删除
delete context.fn
return result
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(3)bind 函数的实现步骤:
判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
保存当前函数的引用,获取其余传入参数值。
创建一个函数返回
函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。
Function.prototype.myBind = function(context) {
// 判断调用对象是否为函数
if (typeof this !== 'function') {
throw new TypeError('Error')
}
// 获取参数
var args = [...arguments].slice(1),
fn = this
return function Fn() {
// 根据调用方式,传入不同绑定值
return fn.apply(this instanceof Fn ? this : context, args.concat(...arguments))
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 如何理解 JavaScript 的执行上下文栈
- 执行上下文
当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。
它有三种执行上下文类型:全局执行上下文、函数执行上下文、Eval 函数执行上下文。
执行上下有三个重要属性
- 变量对象
- 作用域链
- this
- 执行栈
每个函数都会创建执行上下文,执行上下文栈(Execution context stack,ECS)就是 JavaScript 引擎创建出来管理执行上下文的。
栈是一种后进先出的数据结构。当 JS 引擎第一次遇到脚本时,会创建一个全局的执行上下文并且压入当前的执行栈,每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上 下文并压入栈的顶部。引擎会执行那些执行上下文位于栈顶的函数,当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。
- 示例
function foo() {
function bar() {}
bar()
}
foo()
// 假设 ECS 是个数组
// 第一步 初始化, 全局代码的上下文 globalContext
ECS = [globalContext]
// 第二步 执行 foo
ECS = [globalContext, fooContext]
// 第三步 执行 bar
ECS = [globalContext, fooContext, barContext]
//第四步 bar 执行完成, 释放 barContext
ECS = [globalContext, fooContext]
// 第五步 foo 执行完成, 释放 fooContext
ECS = [globalContext]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# ES6
# 你用过哪些 es6 特性
该题作为引子,内容比较发散,考察面试者的 es6 应用广度,如果对某一方面感兴趣,可以继续提问。
- let const 块级作用域
- 解构赋值
- 字符串、数值、函数、数组、对象的扩展和新增方法,如
- 模板字符串``
- includes(),startsWith(),endsWith(),repeat()等
- Number.isNaN()
- 箭头函数,rest 参数,尾调用优化等
- 数组扩展运算符,Array.from(),find(),findIndex(),includes(),flat()等
- 属性简洁表示,对象扩展运算符,Object.assign(),Object.keys()等
- 新增 Symbol 类型
- 新增 Set,Map 数据结构
- Proxy,Relect
- Promise,async/await
- 新增迭代器(Iterator)和生成器(Generator)
- class 取代构造函数,实现继承
- Module 导出和加载
- ...
# let、const、var 的区别
区别 | var | let | const |
---|---|---|---|
是否有块级作用域 | × | ✔️ | ✔️ |
是否存在变量提升 | ✔️ | × | × |
是否添加全局属性 | ✔️ | × | × |
能否重复声明变量 | ✔️ | × | × |
是否存在暂时性死区 | × | ✔️ | ✔️ |
是否必须设置初始值 | × | × | ✔️ |
能否改变指针指向 | ✔️ | ✔️ | × |
解释:
给全局添加属性: 浏览器的全局对象是 window,Node 的全局对象是 global。var 声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是 let 和 const 不会。
暂时性死区: 在使用 let、const 命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。
# const 定义的对象的属性可以修改吗
const 保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动。对于基本类型的数据(数值、字符串、布尔值),其值就保存在变量指向的那个内存地址,因此等同 于常量。
但对于引用类型的数据(主要是对象和数组)来说,变量指向数据的内存地址,保存的只是一个指针,const 只能保证这个指针是固定不变的,至于它指向的数据结构是不是可变的, 就完全不能控制了。
# 箭头函数与普通函数的区别
- 更加简洁。
let fn = () => void doesNotReturn();
- 没有自己的 this。它只会在自己作用域的上一层继承 this,也可以说成是捕获其所在上下⽂的 this 值,作为⾃⼰的 this 值。
- call()、apply()、bind()等方法不能改变箭头函数中 this 的指向
- 箭头函数不能作为构造函数使用。如果 new 一个箭头函数会抛出错误。
- 箭头函数没有自己的 arguments
- 箭头函数没有 prototype
- 箭头函数不能用作 Generator 函数,不能使用 yeild 关键字
# 你是如何理解 Proxy 的
let p = new Proxy(target, handler)
target
代表需要添加代理的对象,handler
用来自定义对象中的操作,比如可以用来自定义 set
或者 get
函数。
下面来通过 Proxy
来实现一个数据响应式:
let onWatch = (obj, setBind, getLogger) => {
let handler = {
get(target, property, receiver) {
getLogger(target, property)
return Reflect.get(target, property, receiver)
},
set(target, property, value, receiver) {
setBind(value, property)
return Reflect.set(target, property, value)
},
}
return new Proxy(obj, handler)
}
let obj = { a: 1 }
let p = onWatch(
obj,
(v, property) => {
console.log(`监听到属性${property}改变为${v}`)
},
(target, property) => {
console.log(`'${property}' = ${target[property]}`)
}
)
p.a = 2 // 监听到属性a改变
p.a // 'a' = 2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
在上述代码中,通过自定义 set
和 get
函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。
当然这是简单版的响应式实现,如果需要实现一个 Vue 中的响应式,需要在 get
中收集依赖,在 set
派发更新,之所以 Vue3.0 要使用 Proxy
替换原本的 API 原因在于
Proxy
无需为每个属性遍历添加代理,一次即可完成以上操作,性能上更好;并且原本的实现有一些数据更新不能监听到,但是 Proxy
可以完美监听到任何方式的数据改
变,唯一缺陷就是浏览器的兼容性不好。
注意,如果是嵌套对象,Proxy 不能一次性监听到,仍需要递归处理。
# commonJs 和 es6 模块化的区别是什么
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块 就是对象,输入时必须查找对象属性。
// CommonJS模块
let { stat, exists, readfile } = require('fs')
// 等同于
let _fs = require('fs')
let stat = _fs.stat
let exists = _fs.exists
let readfile = _fs.readfile
2
3
4
5
6
7
8
上面代码的实质是整体加载fs
模块(即加载fs
的所有方法),生成一个对象(_fs
),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时
才能得到这个对象,导致完全没办法在编译时做“静态优化”。
ES6 模块不是对象,而是通过export
命令显式指定输出的代码,再通过import
命令输入。
// ES6模块
import { stat, exists, readFile } from 'fs'
2
上面代码的实质是从fs
模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加
载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
CommonJs | ES6 | |
---|---|---|
加载模式 | 运行时加载 | 编译时加载 |
模块是否对象 | 是 | 否 |
加载效率 | 低 | 高 |
采用严格模式 | 否 | 自动采用 |
欢迎补充.. |
# 原型与原型链
# 说下构造函数的执行过程
- 使用 new 关键字开辟空间,创建对象
- 调用构造函数,将构造函数中的 this 指向了 new 出来的对象
- 在构造函数内部为对象新增成员(初始化)
- 默认返回了 new 创建出来的对象
# 对原型、原型链的理解
构造函数在被创建出来的时候,系统会默认的帮构造函数创建并且关联一个空对象,这个空对象就是原型。原型中的成员(属性和方法),可以被和这个原型相关的构造函数创建出来 的所有的对象(实例)所共享。
可以通过 2 种方式访问原型
构造函数.prototype
对象.__proto__
一般浏览器中都实现了 __proto__ 属性来访问原型,但是最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法,可以通过 这个方法来获取对象的原型。
原型中默认会有一个属性 constructor,指向和原型相关的构造函数。在给构造函数.prototype 重新赋值的时候,这个 constructor 属性就没有了,除非手动添加。
当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型 链。从语言的层面,原型也是对象,所以原型也有原型,这样就构成原型链。
# 手写一个 new 操作过程
// 模拟new一个对象的过程
/**
* 1.创建一个空对象,继承构造函数的prototype对象
* 2.执行构造函数,传入相关参数,并将this指向第一步的空对象
* 3.如果构造函数返回了对象,则返回该对象,否则返回第一步的对象
*/
function newFn1(constructorFn, ...arg) {
// 第1种
// const obj = {}
// Object.setPrototypeOf(obj, constructorFn.prototype)
// 第2种
// const obj = new Object()
// obj.__proto__ = constructorFn.prototype
// 第3种
const obj = Object.create(constructorFn.prototype)
const resObj = constructorFn.apply(obj, arg)
return resObj instanceof Object ? resObj : obj
}
function Person(n) {
console.log('person')
this.x = n
this.say = function() {
console.log(this.x)
}
}
const p = newFn1(Person, 2)
p.say()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 手动实现一个instanceof
function new_instance_of(leftVaule, rightVaule) {
let rightProto = rightVaule.prototype // 取右表达式的 prototype 值
let leftProto = leftVaule.__proto__ // 取左表达式的__proto__值
while (true) {
if (leftProto === null) {
return false
}
if (leftProto === rightProto) {
return true
}
leftProto = leftProto.__proto__
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 创建对象有几种方式
# 1.工厂模式
在函数内创建对象,给对象赋予属性及方法,再将对象返回
详细解释:工厂模式的主要工作原理是用函数来封装创建对象的细节,从而通过调用函数来达到复用的目的。但是它有一个很大的问题就是创建出来的对象无法和某个类型联系起来 ,它只是简单的封装了复用代码,而没有建立起对象和类型间的关系。
function Person() {
const obj = new Object()
obj.name = '张三'
obj.sayHi = function() {
console.log('hello world')
}
return obj
}
let p1 = Person()
p1.sayHi()
2
3
4
5
6
7
8
9
10
11
# 2.构造函数模式
无需在函数内部重新创建对象,而是用 this 指代,通过 new 实例化对象。
详细解释:js 中每一个函数都可以作为构造函数,只要一个函数是通过 new 来调用的,那么就可以把它称为构造函数。执行构造函数首先会创建一个对象,然后将对象的原型指向 构造函数的 prototype 属性,然后将执行上下文中的 this 指向这个对象,最后再执行整个函数,如果返回值不是对象,则返回新建的对象。因为 this 的值指向了新建的对象, 因此可以使用 this 给对象赋值。构造函数模式相对于工厂模式的优点是,所创建的对象和构造函数建立起了联系,因此可以通过原型来识别对象的类型。但是构造函数存在一个缺 点就是,造成了不必要的函数对象的创建,因为在 js 中函数也是一个对象,因此如果对象属性中如果包含函数的话,那么每次都会新建一个函数对象,浪费了不必要的内存空间, 因为函数是所有的实例都可以通用的。
function Person() {
this.name = '张三'
this.sayHi = function() {
console.log('hello world')
}
}
let p1 = new Person()
p1.sayHi()
2
3
4
5
6
7
8
9
# 3.原型模式
函数中不对属性进行定义,利用prototype
属性对属性进行定义,可以让所有对象实例共享它多包含的属性及方法
因为每一个函数都有一个 prototype 属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例都能共享的属性和方法。因此可以使用原型对象来添加公用属性和方法, 从而实现代码的复用。这种方式相对于构造函数模式来说,解决了函数对象的复用问题。但是这种模式也存在一些问题,一个是没有办法通过传入参数来初始化值,另一个是如果存 在一个引用类型如 Array 这样的值,那么所有的实例将共享一个对象,一个实例对引用类型值的改变会影响所有的实例。
function Person() {
Person.prototype.name = '张三'
Person.prototype.sayHi = function() {
console.log('hello world')
}
}
let p1 = new Person()
p1.sayHi()
2
3
4
5
6
7
8
# 4.混合模式
构造函数模式+原型模式,构造函数模式用于定义属性,原型模式用于定义方法和共享属性
这是创建自定义类型的最常见方式。因为构造函数模式和原型模式分开使用都存在一些问题,因此可以组合使用这两种模式,通过构造函数来初始化对象的属性,通过原型对象来实 现函数方法的复用。这种方法很好的解决了两种模式单独使用时的缺点,但是有一点不足的就是,因为使用了两种不同的模式,所以对于代码的封装性不够好。
function Person() {
this.name = '张三'
}
Person.prototype.sayHi = function() {
console.log('hello world')
}
let p1 = new Person()
p1.sayHi()
2
3
4
5
6
7
8
# 5.动态原型模式
将所有信息封装在了构造函数中,而通过构造函数中初始化原型,这个可以通过判断该方法是否有效而选择是否需要初始化原型
这一种模式将原型方法赋值的创建过程移动到了构造函数的内部,通过对属性是否存在的判断,可以实现仅在第一次调用函数时对原型对象赋值一次的效果。这一种方式很好地对上 面的混合模式进行了封装。
function Person() {
this.name = '张三'
if (typeof Person._sayHi === 'undefined') {
Person.prototype.sayHi = function() {
console.log('hello world')
}
Person._sayHi = true
}
}
let p1 = new Person()
p1.sayHi()
2
3
4
5
6
7
8
9
10
11
# 实现继承的几种方式以及他们的优缺点
# 1.原型链继承
利用Sub.prototype = new Super()
,这样连通了子类-子类原型-父类。
function Super() {
this.flag = true
}
Super.prototype.getFlag = function() {
return this.flag
}
function Sub() {
this.subFlag = true
}
// 实现继承
Sub.prototype = new Super()
// 添加方法,需要在继承之后
Sub.prototype.getSubFlag = function() {
return this.subFlag
}
const sub = new Sub()
const subFlag = sub.getSubFlag()
console.log(subFlag)
const superFlag = sub.getFlag()
console.log(superFlag)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
优点:
- 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
- 父类新增原型方法/原型属性,子类都能访问到
- 简单,易于实现
缺点:
- 要想为子类新增属性和方法,必须要在继承语句之后执行,不能放到构造器中
- 无法实现多继承
- 来自原型对象的引用属性是所有实例共享的
- 创建子类实例时,无法向父类构造函数传参
# 2.构造函数继承
在构造子类构造函数时内部使用call
或apply
来调用父类的构造函数
function Super() {
this.flag = true
}
function Sub() {
//如果父类可以需要接收参数,这里也可以直接传递
Super.call(this)
}
const obj = new Sub()
obj.flag = false
const obj2 = new Sub()
console.log(obj2.flag) //依然是true,不会相互影响
2
3
4
5
6
7
8
9
10
11
12
13
14
优点:
- 解决了原型链继承中,子类实例共享父类引用属性的问题
- 创建子类实例时,可以向父类传递参数
- 可以实现多继承(
call
绑定多个父类对象)
缺点:
- 实例并不是父类的实例,只是子类的实例
- 只能继承父类的实例属性和方法,不能继承原型属性/方法
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
# 3.组合继承
使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承
function Super() {
this.flag = true
}
Super.prototype.getFlag = function() {
return this.flag
}
function Sub() {
this.subFlag = true
// 继承实例属性,第一次调用Super
Super.call(this)
}
//继承父类方法,第二次调用Super
Sub.prototype = new Super()
// 注意,这里Sub.prototype.constructor指向了Super,不符合constructor的定义。因此要加上一行
Sub.prototype.constructor = Sub
Sub.prototype.getSubFlag = function() {
return this.subFlag
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
优点:
- 弥补了原型链继承和构造函数继承的缺点,既可以继承实例的属性和方法,也可以继承原型的属性和方法
instanceof
和isPrototypeOf( )
也能用于识别基于组合继承创建的对象
缺点:
- 调用了两次父类构造函数, 造成了不必要的消耗
# 4.寄生组合继承
寄生组合式继承就是为了降低调用父类构造函数的开销而出现的,不必为了指定子类型的原型而调用超类型的构造函数
function extend(subClass, superClass) {
// 复制父类的原型
const prototype = Object.create(superClass.prototype)
// 将复制的原型的constructor指向子类
prototype.constructor = subClass
// 将子类的prototype指向复制的原型
subClass.prototype = prototype
//指定对象
}
function Super() {
this.flag = true
}
Super.prototype.getFlag = function() {
return this.flag
}
function Sub() {
this.subFlag = true // 继承实例属性,第一次调用Super
Super.call(this)
}
extend(Sub, Super)
Sub.prototype.getSubFlag = function() {
return this.subFlag
}
const sub = new Sub()
console.log(sub.getSubFlag()) // true
console.log(sub.getFlag()) // true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
优点:
- 弥补了组合继承调用两次父类构造函数的缺陷
# 5.Class 继承
class Sub extends Super{}
class 本质上就是原型继承的语法糖,比 ES5 的通过手动修改原型链实现继承,要清晰和方便很多。
# 异步编程
# JavaScript 中的异步执行有哪些形式
- 回调函数。多个回调嵌套会造成回调地狱,不利于维护。
- Promise。将回调转成链式调用,多个 then 链式调用,也不利于维护。
- generator。结合 yield 关键字,可以暂停/开始执行代码,开始执行异步时暂停,等异步执行完毕恢复执行,优点是可以用同步的方式书写异步函数。
- async/await。该方式是 generator 和 generator 实现的语法糖。它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函 数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。
# 怎么创建 Promise 对象?
一般情况下都会使用new Promise()
来创建 promise 对象,但是也可以使用promise.resolve
和promise.reject
这两个方法。
Promise.resolve(x)
可以看作是 new Promise(resolve => resolve(x))
的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。
# Promise(构造函数)方法有哪些?
Promise 有五个常用的方法:then()、catch()、all()、race()、finally。
- then()
then
方法可以接受两个回调函数作为参数。第一个回调函数是 Promise 对象的状态变为resolved
时调用,第二个回调函数是 Promise 对象的状态变为rejected
时调用。其中第
二个参数可以省略。 then
方法返回的是一个新的 Promise 实例(不是原来那个 Promise 实例)。因此可以采用链式写法,即then
方法后面再调用另一个 then 方法。
let promise = new Promise((resolve, reject) => {
ajax('first').success(function(res) {
resolve(res)
})
})
promise
.then((res) => {
return new Promise((resovle, reject) => {
ajax('second').success(function(res) {
resolve(res)
})
})
})
.then((res) => {
return new Promise((resovle, reject) => {
ajax('second').success(function(res) {
resolve(res)
})
})
})
.then((res) => {})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- catch()
该方法相当于then
方法的第二个参数,指向reject
的回调函数。不过catch
方法还有一个作用,就是在执行resolve
回调函数时,如果出现错误,抛出异常,不会停止运行,而
是进入catch
方法中。
p.then(
(data) => {
console.log('resolved', data)
},
(err) => {
console.log('rejected', err)
}
)
p.then((data) => {
console.log('resolved', data)
}).catch((err) => {
console.log('rejected', err)
})
2
3
4
5
6
7
8
9
10
11
12
13
- all()
all
方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个promise
对象。当数组中所有的promise
的状态都达到resolved
的时候,all
方法的状态就会变
成resolved
,如果有一个状态变成了rejected
,那么all
方法的状态就会变成rejected
。
let promise1 = new Promise((resolve, reject) => { setTimeout(() => { resolve(1); }, 2000);});let promise2 = new Promise((resolve, reject) => { setTimeout(() => { resolve(2); }, 1000);});let promise3 = new Promise((resolve, reject) => { setTimeout(() => { resolve(3); }, 3000);});Promise.all([promise1, promise2, promise3]).then(res => { console.log(res); //结果为:[1,2,3]});
- race
race
方法和all
一样,接受的参数是一个promise
的数组,但是与all
不同的是,当最先执行完的事件执行完之后,就直接返回该promise
对象的值。如果第一个promise
对
象状态变成resolved
,那自身的状态变成了resolved
;反之第一个promise
变成rejected
,那自身状态就会变成rejected
。
let promise1 = new Promise((resolve,reject)=>{ setTimeout(()=>{ reject(1); },2000)});let promise2 = new Promise((resolve,reject)=>{ setTimeout(()=>{ resolve(2); },1000)});let promise3 = new Promise((resolve,reject)=>{ setTimeout(()=>{ resolve(3); },3000)});Promise.race([promise1,promise2,promise3]).then(res=>{ console.log(res); //结果:2},rej=>{ console.log(rej)};)
那么race
方法有什么实际作用呢?当要做一件事,超过多长时间就不做了,可以用这个方法来解决:
Promise.race([promise1, timeOutPromise(5000)]).then((res) => {})
- finally()
finally
方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
# Promise.all 和 Promise.race 的区别的使用场景
(1)Promise.all Promise.all
可以将多个Promise
实例包装成一个新的 Promise 实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失
败的时候则返回最先被 reject 失败状态的值。
Promise.all 中传入的是数组,返回的也是是数组,并且会将进行映射,传入的 promise 对象返回的值是按照顺序在数组中排列的,但是注意的是他们执行的顺序并不是按照顺序的 ,除非可迭代对象为空。
需要注意,Promise.all 获得的成功结果的数组里面的数据顺序和 Promise.all 接收到的数组顺序是一致的,这样当遇到发送多个请求并根据请求顺序获取和使用数据的场景,就可 以使用 Promise.all 来解决。
(2)Promise.race
顾名思义,Promse.race 就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。当要做一件 事,超过多长时间就不做了,可以用这个方法来解决:
Promise.race([promise1, timeOutPromise(5000)]).then((res) => {})
# async/await 如何捕获异常
async function fn() {
try {
let a = await Promise.reject('error')
} catch (error) {
console.log(error)
}
}
2
3
4
5
6
7
# 你对 setTimeout、setInterval 和 requestAnimationFrame 的理解
- 定义:
**setTimeout()
**方法设置一个定时器,该定时器在定时器到期后执行一个函数或指定的一段代码。
setInterval()
方法重复调用一个函数或执行一个代码段,在每次调用之间具有固定的时间延迟。
requestAnimationFrame()
告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调
函数会在浏览器下一次重绘之前执行。
注意:
setInterval
并不考虑任务执行本身所消耗的时间,例如,如果指定每 100ms 执行一次,每次执行需要 5ms,那么第一次执行结束后 95ms,第二次执行就会开始,也就是说这个时
间间隔小于设定的时间。如果执行任务长时间不执行完,超过了间隔时间,那么在执行完任务的时候,就会跳过时间间隔又立即执行任务。由于它存在执行累积的问题,通常不建议使
用 setInterval()。
- 取消执行:
setTimeout()和 setInterval()都可以取消执行,分别是 clearTimeout(timeoutId)和 clearInterval(intervalId)。
- 区别
相比 setTimeout 和 setInterval,requestAnimationFrame 在执行动画时有以下优势:
- requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率。
- 在隐藏或不可见的元素中,requestAnimationFrame 将不会进行重绘或回流,这当然就意味着更少的 CPU、GPU 和内存使用量。
- requestAnimationFrame 是由浏览器专门为动画提供的 API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了 CPU 开销 。
# 描述 EventLoop(事件循环)过程
EventLoop 实现过程:
- 所有同步任务都在主线程上执行,形成一个执行栈。
- 主线程之外,有一个任务队列(Task Queue),只要异步任务有了运行结果,就在任务队列中进行等待。
- 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,那些对应的异步任务就结束等待状态,进入执行栈,开始执行。
- 当前任务执行完毕,会查看任务队列中是否还有异步回调在等待,如果有,则继续执行。
- 这个过程不断循环,所以就是事件循环。
任务队列可分为宏任务和微任务,任务执行优先级:同步任务>微任务>宏任务。
# 宏任务和微任务分别有哪些
- 宏任务
setTimeout、setInterval、Promise 中的 executor
- 微任务
Promise.then
process.nextTick(node 中)