引言
嘿! 欢迎来到我整理的 JS
面试题集!! 无论你是正在寻找前端开发的机会, 还是想巩固自己的 JS
知识, 相信这些问题会给你带来些帮助或启发!!!
考虑到 JS
相关面试内容会比较多, 所以这里会将其拆分多个篇幅进行讲解!!! 没篇固定十道题目!!!
现在, 让我们一起开始吧!!
一、原型、原型链
参考: 原型、原型链
- 原型本质上是一个对象, 存在于每个
非箭头函数
中, 所以每个非箭头函数
都有一个属性prototype
指向原型对象

- 原型对象、构造函数、实例对象三者之间的关系
- 构造函数
prototype
指向原型对象
- 原型对象
constructor
指向构造函数
- 实例对象
__proto__
指向原型对象

-
所有
非空类型
数据, 都具有原型对象
, 因为从本质上它们都是通过对应构造函数
构建出来的, 所以它们都具有__proto__
属性, 指向构造函数
的原型对象
-
要判断某个值其
原型对象
, 只需要确认该值是通过哪个构造函数
构建的即可, 只要确认了构造函数
那么该值的__proto__
必然指向该构造函数
的prototype
-
原型链: 根据上文, 所有
非空数据
, 都可以通过__proto__
指向原型对象
, 同时如果原型对象
非空, 那么必然同样会有__proto__
指向它自己的原型对象
, 如此一层层往上追溯, 以此类推, 就形成了一整条链路, 一直到某个原型对象
为null
, 才到达最后一个链路的最后环节,原型对象
之间这种链路关系
被称之为原型链(prototype chain)

原型链
最后都会到Object.prototype
, 因为原型对象
, 本质上就是个对象, 由Object
进行创建, 其__proto__
指向Object.prototype
, 同时约定Object.prototype.__proto__
等于null
, 所有原型链的终点都已null
结束

- 作用:
- 实现继承: JS 中继承主要就是通过原型、原型链来实现的
- 为某一类型数据设置共享属性、方法, 将大大
节约内存
- 查找属性: 当我们试图访问
对象属性
时, 它会先在当前对象
上进行搜寻, 搜寻没有结果时会继续搜寻该对象的原型对象
, 以及该对象的原型对象
的原型对象
, 依次层层向上搜索, 直到找到一个名字匹配的属性或到达原型链的末尾
-
补充说明:
__proto__
并不是ECMAScript
语法规范的标准, 它只是大部分浏览器厂商实现或说是支持的一个属性, 通过该属性方便我们访问、修改原型对象, 从ECMAScript 6
开始, 可通过Object.getPrototypeOf()
和Object.setPrototypeOf()
来访问、修改 原型对象 -
总结(来自高程): 每个
构造函数
都有一个原型对象
,原型
有一个属性指回构造函数
, 而实例
有一个内部指针
指向原型
; 如果原型
是另一个类型的实例呢? 那就意味着这个原型
本身有一个内部指针
指向另一个原型
, 相应地另一个原型
也有一个指针
指向另一个构造函数
二、常用继承方案
2.1 原型链继承
- 思路: 通过将
子类
的原型对象
指向父类
的实例对象
来实现继承
- 缺点:
原型
如果包含引用值
, 修改引用值
所有实例
都会改动到 - 缺点:
子类
在实例化
时不能给父类
的构造函数
传参
2.2 盗用构造函数
- 思路: 在
子类构造函数
中, 调用父类构造函数
来实现继承 - 优点: 可解决上文提到的
引用值
问题, 每个实例
都是新建一个引用值
- 优点: 支持为父类构造函数传参
- 缺点: 不能共用属性、方法, 每次都是重新创建
- 缺点:
instanceof
操作符和isPrototypeOf()
方法无法识别出合成对象
继承于哪个父类
2.3 组合继承
- 思路: 使用
原型链
继承原型上的属性和方法, 通过盗用构造函数
继承父类属性 - 优点:
组合继承
弥补了原型链继承
和盗用构造函数继承
的不足 - 优点:
组合继承
也保留了instanceof
操作符和isPrototypeOf()
方法识别合成对象
的能力 - 缺点: 存在效率问题, 在为
子类
设置原型
时会额外调用一次父类构造函数
, 会创建无效的属性、方法
2.4 原型式继承
- 思路: 通过一个
函数
, 函数内部会创建一个临时构造函数
, 并将传入的对象
作为这个构造函数的原型
, 最后返回这个临时类型的一个实例
来实现继承 - 适用场景: 基于现有的一个对象, 的基础之上创建新的对象, 并进行适当的修改
- 缺点: 跟使用
原型模式
类似, 在原型式继承
中,引用值属性
始终会在相关对象间共享
- 补充:
ES5
通过增加Object.create()
方法将原型式继承
的概念规范化, 和原型链继承
的区别在于原型式继承
不需要自定义类型, 直接通过一个函数来实现继承
2.5 寄生式继承
- 思路:
寄生式继承
与原型式继承
很接近, 背后的思路类似于寄生构造函数模式
和工厂模式
, 通过创建一个实现继承的函数
, 以某种方式增强对象, 然后返回这个对象 - 适用场景:
寄生式继承
同样适合主要关注对象, 而不在乎类型
和构造函数
的场景, 其中Object.create()
函数不是寄生式继承
所必需
的, 任何返回新对象的函数都可以在这里使用 - 缺点: 通过
寄生式继承
给对象添加的函数, 难以被复用
2.6 寄生式组合继承
- 思路: 在
组合继承
的思想基础之上进行优化, 修改子类原型
时不再直接创建父类的实例, 而是通过寄生式继承
来继承父类原型
, 然后将返回的新对象作为子类原型 - 优点: 使用
寄生式继承
来弥补组合继承
的缺点, 设置子类原型时只会对父类原型进行拷贝, 而不是创建父类实例对象, 这样可以避免创建无用是属性、方法,寄生式组合继承
可以算是引用类型
继承的最佳模式
2.7 Class 继承
- 思路: 使用
extends
关键字, 继承任何拥有[[Construct]]
和原型
的对象, 很大程度上, 这意味着不仅可以继承
一个类
, 也可以继承
普通的构造函数
(向后兼容) - 优点: 上文提到的各种继承策略都有自己的问题, 也有相应的妥协, 通过
class
语法糖, 可以轻松实现继承, 避免代码显得非常冗长
和混乱
三、New 运算符做了哪些事情
- 创建一个新的空对象
A
- 往空对象挂载
构造函数 Com
的原型对象
: 对象A
创建__proto__
属性, 并将构造函数
的prototype
属性赋值给__proto__
- 执行
构造函数 Com
: 改变构造函数
this
指向, 指向空对象A
, 并执行构造函数
, 往空对象注入属性 - 判断
构造函数
是否返回一个对象?
- 是: 如果
构造函数
也返回了一个对象B
, 则最终new
出来的对象则为返回的对象B
- 否: 最终
new
出来的对象为最初创建的对象A
因此当我们执行
js
var o = new Foo();
实际上执行的是:
js
// 1. 创建一个新的空对象 A
let A = {};
// 2. 往空对象挂载, 挂载构造函数 Com 的原型对象: obj.__proto__ === Con.prototype;
Object.setPrototypeOf(A, Con.prototype);
// 3. 执行构造函数: 改变构造函数 this 指向, 指向对象 A, 往 A 注入属性
let B = Con.apply(A, args);
// 4. 判断构造函数是否返回对象: 是则取返回值、否则取最初创建的对象 A
const newObj = B instanceof Object ? res : A;
手写一个 myNew
函数, 实现上诉操作
js
const myNew = (Com, ...args) => {
// 1. 创建一个新的空对象 A
let A = {};
// 2. 往空对象挂载, 挂载构造函数 Com 的原型对象: obj.__proto__ === Con.prototype;
Object.setPrototypeOf(A, Con.prototype);
// 3. 执行构造函数: 改变构造函数 this 指向, 指向对象 A, 往 A 注入属性
let B = Con.apply(A, ...args);
// 4. 判断构造函数是否返回对象: 是则取返回值、否则取最初创建的对象 A
const newObj = B instanceof Object ? res : A;
return newObj
}
四、常见的数据类型
4.1 常见类型(8种)

补充:
symbol
: 主要用于创建一些唯一标识, 可作为对象的属性名使用, 可通过Symbol('xxx')
进行声明bigInt
: 用于表示比Number
大的数值(-2^53 - 1 到 2^53 - 1)
, 它声明方式有两种:- 字面量形式, 通过在数字后面加
n
来进行表示:const bigint = 123n
- 通过函数
BigInt
来声明:const bigint = BigInt(12312)
- 字面量形式, 通过在数字后面加
Symbol
BigInt
都不是一个构造函数, 所以new
关键词的方式来构建实例- 创建一个
BigInt
的时候, 参数必须为整数, 否则或报错
4.2 为什么 Symbol 和 bigInt 不支持 new
原因: 我们都知道通过 new
操作符将创建一个对象, 也就是一个引用类型, 但是 Symbol
和 BigInt
应该是要是一个基本类型, 所以它们禁用掉 new
是为了避免创建 Symbol
或 BigInt
时被包装为对象, 因为绝大部分情况下我们并不需要去创建它们值的对象
倔强: 如果一定要创建 Symbol
或 BigInt
原始值的对象, 可以怎么做? 可以使用 Object
包装下
js
let mySymbol = Symbol()
mySymbol // Symbol()
typeof mySymbol // "symbol"
let myWrappedSymbol = Object(mySymbol)
myWrappedSymbol // Symbol {Symbol()}
typeof myWrappedSymbol // "object"
怎么做到: 上文我们说到, 使用 new
操作符会创建一个空对象, 然后修改函数的 this
的一个指向并执行, 那么我们可以借用这个特性, 通过判断 this
来确认该函数是否是通过 new
操作符进行调用
js
// 模拟实现
function A() {
console.log(this)
if (this instanceof A) {
throw new Error('Uncaught TypeError: A is not a constructor')
}
return ''
}
A() // window ''
new A() // A {} Uncaught TypeError: A is not a constructor
4.3 undefind 和 null
Undefined
类型表示未定义, 该类型只有一个值undefined
, 任何变量在赋值前都是Undefined
类型数据, 其值为undefined
- 需要注意的是
JavaScript
的代码undefined
是一个变量, 而并非是一个关键字, 这是JavaScript
语言公认的设计失误之一 - 当我们未给变量赋值时, 变量默认值为
window.undefind
(浏览器环境下),window.undefind
该属性值无法被修改 - 当我们定义一个变量, 并显性的设置其值为
undefined
时, 该值一般情况指的都是全局变量undefined
, 但由于undefined
是变量而不是关键字, 在作用域内我们完全可以对undefined
变量进行篡改, 重新定义, 所以为了避免无意中被篡改, 建议使用void 0
来获取undefined
值
js
(function () {
var undefined = 1;
var a = undefined;
console.log('[ a ]', a); // 1
})();
(function () {
var undefined = 1;
var a = void 0;
console.log('[ a ]', a); // undefined
})();
-
Undefined
跟Null
有一定的表意差别,Null
表示定义了但是为空
, 故而在实际编程时, 我们一般不会把变量直接显性的赋值为undefined
, 这样可以保证所有值为undefined
的变量, 都是真正意义上的未赋值状态 -
Null
类型也只有一个值null
, 它的语义表示空值, 与undefined
不同,null
是JavaScript
关键字, 所以在任何代码中都可以放心用null
关键字来获取null
值
五、变量提升、暂时性死区、var、let、const
- 变量都会提升: 在创建变量时, 无论在哪里进行声明,
变量的声明都会被提升
至当前作用域顶部, 就相当于在顶部进行声明, 需要注意的是初始化还是在原本地方进行初始化
- 使用
let
cont
在初始化之前变量是无法被操作的, 这一现象被被称为暂存死区
js
// 声明全局变量
var a = 1;
var b = 3;
// 访问全局变量
console.log(a); // 1
console.log(window.a); // 1
// 声明函数作用域内的变量
function fun () {
console.log(b) // undefined
var b = 2; // 只属于 fun函数的变量
console.log(b); // 2
};
fun(); // 2
console.log(b); // 3
let
const
定义的全局变量, 并不会挂载在顶层对象(globalThis
)上, 而是挂载在Script
作用域上
在
ES5
中,var
操作符和function
操作符声明的全局变量, 会被挂载到顶层对象(globalThis
), 也就是说顶层对象的属性
和全局变量
是等价的,ES6
中let
、const
、class
操作符声明的全局变量, 不再挂载在顶层对象(globalThis
)下, 而是会被挂载在Script
作用域下

特性 | var | let | const |
---|---|---|---|
变量提升 | √ | √ | √ |
重复声明 | √ | X | X |
是否可以更改变量值 | √ | √ | X |
块级作用域 | X | √ | √ |
全局定义是否挂载在顶层对象(globalThis )下 |
√ | X | X |
六、事件
6.1 事件流
事件流描述的是页面中接受事件的顺序, 主要分为三个阶段分别是 事件捕获阶段
目标阶段
事件冒泡阶段
事件流首先是经过事件捕获阶段、接着是目标阶段、最后是事件冒泡阶段, 如下图

- 事件冒泡: 事件触发是从内往外冒泡, 比如已知嵌套元素
<A><B><C></C></B></A>
为每个元素绑定点击事件并开启冒泡, 则点击C
将先后触发C
B
A
各个元素的点击事件 - 事件捕获: 事件触发是从外往内冒泡, 比如已知嵌套元素
<A><B><C></C></B></A>
为每个元素绑定点击事件并开启捕获, 则点击C
将先后触发A
B
C
各个元素的点击事件
下面代码演示代码, 当点击按钮时, 控制台将输出事件在各个阶段被触发的顺序
html
<style>
.first {
width: 200px;
height: 200px;
background-color: #0000cc;
}
.second {
width: 100px;
height: 100px;
background-color: yellow;
}
</style>
<div class="first">
<div class="second">
<input type="button" value="click" class="btn" />
</div>
</div>
<script type="text/javascript">
window.onload = function () {
register();
};
function register() {
// 获取div以及button
let div1 = document.getElementsByClassName('first')[0];
let div2 = document.getElementsByClassName('second')[0];
let btn = document.getElementsByClassName('btn')[0];
// 为三个目标标签注册点击事件 冒泡
div1.addEventListener('click', func);
div2.addEventListener('click', func);
btn.addEventListener('click', func);
// 为三个目标标签注册点击事件 捕获
div1.addEventListener('click', func, true);
div2.addEventListener('click', func, true);
btn.addEventListener('click', func, true);
}
function func() {
let dict = { '1': '捕获阶段', '2': '处于目标阶段', '3': '冒泡阶段' };
console.log(
'[ 事件流 ]',
`${this.className.toLowerCase()} 的事件在 ${dict[event.eventPhase]} 被触发`
);
}
</script>
6.2 事件委托
利用冒泡的原理, 将需要绑定在子元素上的事件, 绑定在父元素或祖先元素上, 当子元素触发事件时, 事件冒泡到父元素上或祖先元素上, 在事件中如果需要可根据事件对象(
event
)来根据不同事件类型、事件目标进行逻辑处理
html
<style>
.p {
padding: 20px;
border: 1px solid yellow;
}
.c {
margin: 5px;
height: 40px;
border: 1px solid red;
}
</style>
<div class="p">
<div class="c">0</div>
<div class="c">1</div>
<div class="c">2</div>
<div class="c">3</div>
<div class="c">4</div>
<div class="c">5</div>
<div class="c">6</div>
<div class="c">7</div>
<div class="c">8</div>
<div class="c">9</div>
</div>
<script>
// 点击事件
const onClick = e => {
console.log('[ 点击 ]', e)
};
// 绑定事件
const bindEvent = () => {
const dom = document.getElementsByClassName('p')[0];
dom.addEventListener('click', onClick);
};
window.onload = () => {
bindEvent();
};
</script>
- 使用场景: 需要为许多同级元素绑定相同事件函数时, 就可以利用
事件委托
, 将它们用一个父元素包裹, 并为父元素绑定事件来统一处理 - 优点: 统一处理时间大大提高了 JS 性能、可以动态添加
DOM
元素, 不需要因为元素的变动而修改事件绑定 - 注意: 事件委托绑定的元素, 最好是被监听元素的父元素, 因为
事件冒泡
的过程也要耗时, 越接近顶层也就越耗时
七、事件循环、宏任务、微任务
7.1 简述
Event loop
也就是所谓事件循环:
- 不是
JS
本身的一个机制, 而是JS
所运行环境的一个机制 - 作用是为了协调事件、用户交互、脚本、渲染、网络等等
- 存在一个
消息队列
用于存储任务, 每当用户进行交互时, 比如用户点击、资源加载等,IO 线程
就会往队列中添加任务 - 同时它将所有任务又可分为
宏任务
和微任务
, 当然对应的也就有了宏任务列表
和微任务列表
- 任务队列是由
event loop
来管理, 然后任务的执行则由js
引擎进行执行 - 那么执行顺序: 主线程则不断的从消息队列中查询、获取任务并进行执行, 它会先
执行一个宏任务
, 然后执行所有微任务
、 再执行一个宏任务
...... 如此循环往复 - 当然在事件循环中, 在检查任务时如果发现
没有
宏任务了, 那么将跳过宏任务的执行, 直接执行所有微任务
- 需要注意的是, 在执行任务过程中也会不断的往队列中新增任务
- 一个宏任务标志了一个
loop
(循环) 的开始

7.2 宏任务包含哪些
JS
代码 (可以理解为外层同步代码或主线程代码)- 用户交互事件(如鼠标点击、滚动页面、放大缩小等)
setTimeout/setInterval
(计时器)- 页面渲染(解析
DOM
、计算布局、绘制) - 页面跨源通信(
postMessage
)、与子进程间通信(MessageChannel
) - 网络请求回调、文件读写回调
Promis
: 主体部分, 也就是Promise(fun)
中的函数参数fun
7.3 微任务包含哪些
Promise
: 所有涉及到状态变更后才被执行的回调都算微任务, 比如说then
、catch
、finally
MutationObserver
(监听DOM
变化)process.nextTick
(Node.js
)
7.4 async 和 await 是如何处理异步任务
实际上
async
await
只是Promise
的语法糖
async
函数返回一个Promise
await
后面的函数等同于New Promise()
里的参数(函数)await
下一行语句, 等同于.then()
函数里待执行语句
js
// async await 语法如下
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
// 等价于
new Promise((resolve, reject) => {
// 执行 await 后的语句
async2();
// .....
}).then(() => {
// 执行a sync1() 函数 await 之后的语句
console.log('async1 end')
});
7.5 几个题目
- 题目一: 最终打印输出
code
promise1
promise2
timeout
js
/*
1. 最开始微任务列表为 [], 宏任务列表为: [主线程(整个代码块)]
2. 基于事件循环(不断从任务列表中获取任务), 先执行执宏任务(整块代码):
- 执行 setTimeout 将回调函数 `() => console.log('timeout')` 添加到宏任务列表
- 分别执行 promise 将两个回调函数 `() => console.log('promise1')` `() => console.log('promise2')` 加入微任务列表
- 最后打印出 code
3. 基于事件循环(不断从任务列表中获取任务), 先执行所有微任务, 打印出 `promise1` `promise2`, 再执行下一个宏任务, 打印出 `timeout`
4. 继续事件循环, 不断查询任务列表、只要有任务则继续执行
*/
setTimeout(() => console.log('timeout'));
Promise.resolve()
.then(() => console.log('promise1'));
Promise.resolve()
.then(() => console.log('promise2'));
console.log('code');
- 题目二: 最终打印输出 1 3 4 7 5
promise in setTimeout1
then in setTimeout1
setTimeout2
js
/**
1. 最开始微任务列表为 [], 宏任务列表为: [主线程(整个代码块)]
2. 基于事件循环(不断从任务列表中获取任务), 先执行执宏任务(整块代码):
- 打印 1
- 执行 setTimeout 10 秒后将回调函数加到宏任务
- 执行 Promise, 执行 new Promis() 执行参数函数 打印 3 4, 后将回调函数 .then 部分加到微任务
- 执行 setTimeout 10 秒后, 将回调函数加到宏任务
- 打印 7
3. 基于事件循环(不断从任务列表中获取任务), 先执行所有微任务: 打印 5, 执行下一个宏任务: 执行 Promise, 执行 new Promis() 执行参数函数, 打印 promise in setTimeout1, 后将回调函数 .then 部分加到微任务
4. 基于事件循环(不断从任务列表中获取任务), 执行所有微任务: 打印 then in setTimeout1, 执行下一个宏任务: 打印 setTimeout2
5. 继续事件循环, 不断查询任务列表、只要有任务则继续执行
*/
console.log(1)
// setTimeout1
setTimeout(function () {
new Promise(function (resolve) {
console.log('promise in setTimeout1')
resolve()
}).then(function () {
console.log('then in setTimeout1')
})
}, 10)
new Promise(function (resolve) {
console.log(3)
for (var i = 100000; i > 0; i--) {
i == 1 && resolve()
}
console.log(4)
}).then(function () {
console.log(5)
})
// setTimeout2
setTimeout(function () {
console.log('setTimeout2')
}, 10)
console.log(7)
- 题目二: 最终打印输出
script start
async2 end
Promisescript end
async1 end
promise1 promise2 setTimeout
js
/*
1. 最开始微任务列表为 [], 宏任务列表为: [主线程(整个代码块)]
2. 基于事件循环(不断从任务列表中获取任务), 先执行宏任务(整块代码):
- 打印 script start
- 执行 async1, async1 本质上是一个 Promise, 先执行 await 后的函数, 打印出 async2 end, 后将 await 后的代码等同于 Promise.then 的回调函数, 添加到微任务中
- 执行 setTimeout, 将回调函数加到宏任务
- 执行 Promise, 先执行参数函数打印 Promise, 后将 .then 部分加入微任务, 注意这里有两个 .then 都需要加入任务
- 打印 script end
3. 基于事件循环(不断从任务列表中获取任务), 先执行所有微任务: 打印 async1 end、promise1、promise2, 执行下一个宏任务: 打印 setTimeout
5. 继续事件循环, 不断查询任务列表、只要有任务则继续执行
*/
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
7.6 requestAnimationFrame 既不是宏任务也不是微任务
特性:
- 当开始执行它的回调时, 在此刻之前注册的所有该类回调, 会一次性执行完(一个
loop
内, 这点很关键) - 当该类任务执行完后, 也会执行所有微任务(其实不能称为特性, 毕竟所有脚本任务执行完都要执行所以微任务)
执行时机: 触发时机总是与浏览器的 渲染频率
保持一致
其实, 从规范中, 至始至终找不到宏任务的描述。宏任务的概念, 应该是社区为了区别微任务, 而创造出来的。那么
宏任务
这个概念是否还有意义呢?我认为还是有意义的。它的意义在于前述的执行时机: 一个loop
的起始阶段, 且一个宏任务标志了一个loop
, 所以我们提宏任务时, 就是指那些在loop
开始时会去检查的任务, 如此这个名称是有其独特意义的
7.7 参考
- 说说事件循环(浏览器和
Node
) - 彻彻底底搞明白, 什么是宏任务, 什么是微任务, 什么是事件循环Event-loop
- 浅聊事件循环:宏任务和微任务
- 宏任务和微任务到底是什么?
- requestAnimationFrame回调在HTML的Event Loop中是一个宏任务么?
八、this 指向问题
所谓 this
其实指的是当前代码所处的上下文, 执行的环境
8.2 全局上下文
非严格模式和严格模式中, 全局 this
始终指向顶层对象 (浏览器中是 window
)
js
this === window // true
'use strict'
this === window;
this.name = 'myj';
console.log(this.name); // myj
8.1 普通函数
普通函数的 this
在调用时绑定的, 完全取决于函数的调用位置(也就是函数的调用方法), this
总是指向调用该函数的对象
- 全局调用(直接调用), 在非严格模式下指向顶层对象(浏览器中是
window
), 在严格模式下等于undefind
js
// 非严格模式
function fun () {
console.log('this', this)
}
fun() // this Window {}
js
// 严格模式
"use strict";
function fun () {
console.log('this', this)
}
fun() // this undefined
- 通过对象调用, 函数内
this
指向该对象
js
const obj = {
name: 'myj',
getName(){
console.log(this.name)
}
}
obj.getName() // myj
- 对象内方法被重新赋值声明, 那么其实等价于全局调用, 这里将沿用全局调用规则
js
const obj = {
name: 'myj',
getName(){
console.log(this.name)
}
}
const fun = obj.getName
fun() // undefined
- 函数被另一个函数被调用, 本质上还是被直接调用, 也将沿用全局调用规则
js
function a () {
console.log('this', this)
}
const obj = {
name: 'myj',
getName(){
console.log(this.name)
a() // this Window {}
}
}
obj.getName() // myj
- 到这里, 我们很容易知道: 匿名函数、定时器、大部分回调函数它们基本都是全局调用的, 所以沿用全局调用规则
js
(function () {
console.log('匿名函数: ', this) // 匿名函数: window {}
})()
setTimeout(() => {
console.log('定时器: ', this) // 定时器: window {}
}, 0)
Promise.resolve().then(() => {
console.log('Promise: ', this) // Promise: window {}
})
9.2 箭头函数
箭头函数没有自己的 this
, 或者说箭头函数中 this
的值 始终
等于 声明时
所处上下文的 this
指向
js
const obj = {
name: 'myj',
getName: () => {
console.log(this.name)
}
}
obj.getName() // undefinde
九、箭头函数和普通函数的区别
9.1 语法(写法)
箭头函数在函数声明上更加简洁
js
// 箭头函数
const fun = () => {}
// 普通函数
const fun = function () {}
9.2 参数差异: 箭头函数没有 arguments 绑定
规定严格模式下不允许使用
arguments
- 普通函数里
arguments
代表了调用时传入的参数列表
js
const arguments = { name: 'myj' }
const fun = function(){
console.log(arguments);
}
fun(1, 2, 3); // { '0': 1, '1': 2, '2': 3 }
- 箭头函数会把
arguments
当成一个普通的变量, 顺着作用域链由内而外地查询
js
const arguments = { name: 'myj' }
const fun2 = () => {
console.log(arguments);
}
fun2(1, 2, 3); // { name: 'myj' }
- 在
ES6
中arguments
可以用...rest
取代, 所以完全没必要追求argument
js
const fun = function(...rest){
console.log(rest)
}
fun(1, 2, 3); // [1, 2, 3]
const fun2 = (...rest) => {
console.log(rest);
}
fun2(1, 2, 3); // [1, 2, 3]
9.3 this 指向不同
不像普通函数, 箭头函数没有自己的 this
, 或者说箭头函数中 this
的值 始终
等于 声明时
所处上下文的 this
指向
js
const obj1 = {
name: 'obj1',
getThis(){
console.log('obj1', this)
}
}
const obj2 = {
name: 'obj2',
getThis: () => {
console.log('obj2', this)
}
}
obj1.getThis() // obj1 {name: 'obj1', getThis: ƒ}
obj2.getThis() // obj2 Window {...}
- 使用
call
apply
bind
都是无法直接修改this
指向, 因为箭头函数中this
指向始终
等于声明时所处上下文的this
指向
js
const obj = { name: 'myj' }
const fun1 = function () {
console.log('fun1', this)
}
const fun2 = () => {
console.log('fun2', this)
}
fun1.call(obj) // fun1 { name: 'myj' }
fun2.call(obj) // fun2 Window {...}
- 当然我们可以通过一些特殊手段来修改箭头函数的
this
指向: 通过改变封包环境
js
function closure(){() => {
// code
}}
closure.call(another)
9.4 没有原型 prototype
我们都知道普通函数中, 都会有一个 prototype
指向原型对象
js
function fun () {}
console.log(fun.prototype) // { constructor: ƒ }
特别的是, 箭头函数是没有 prototype
属性的
js
const fun = () => {}
console.log(fun.prototype) // undefined
那么为什么没有 prototype
属性? 因为 this
指向问题(具体看下面解答👇🏻), 所以箭头函数不应该被作为构造函数使用, 所以也就没必要有该属性(这一块纯属个人猜测)
9.5 不可作为构造函数使用
我们都知道普通函数可以被作为构造函数进行使用
js
function A () {
this.name = 'myj'
this.age = 18
}
new A() // { name: 'myj', age: 18 }
但是呢由于, 箭头函数 this
始终
等于声明时所处上下文, 并且无法被直接修改, 所以是不应该被作为构造函数进行使用的, 同时它没有 prototype
属性所以在使用 new
操作符的情况下将会抛出错误
js
const A = () => {}
new A()

9.6 箭头函数不能有重复的参数命名
在普通函数只有在严格模式下才不允许使用重复的参数命名, 但是对于箭头函数来说, 不论在严格模式还是非严格模式参数重复命名都会报错
js
// 严格模式下才会报错
function add(x, x) {}
// 箭头函数不论在严格模式还是非严格模式重复命名都会报错
const fun = (x, x) => {}

十、call apply bind
10.1 相同点
- 都可改变
this
指向 - 第一参数都是要修改的
this
指向 - 第一参数如果是
null
或者undefined
则默认为window
10.2 差异
call
apply
都会立即执行函数,bind
则是会返回一个新的函数call
和bind
第二个参数开始, 依次接收多个参数,apply
第二参数是个数组(个人理解: 毕竟人家是a
开头的, 表示array
)
10.2 箭头函数 this 指向
箭头函数是 JS
中的一种语法糖, 它具有简洁的语法, 同时箭头函数它的 this
上下文是固定的, 不能直接被改变, 可以间接被修改(上文有提到!)
10.3 源码实现
js
// call 方法实现: 重点是将当前方法(this) 挂载到 context 后执行
// 1. 在函数原型 (Function.prototype) 上挂载方法
Function.prototype.myCall = function(context, ...args) {
// 2. 处理函数执行上下文, 判断传入的上下文是否是一个对象, 如果不是默认为 window
context = (context === undefined || context === null) ? window : Object(context)
// 3. 将当前方法挂载到上下文中, 使用 symbol 作为 key 避免属性冲突
const funKey = Symbol();
context[funKey] = this;
// 4. 执行上下文中挂载的方法, 并透传参数
var result = context[funKey](...args);
// 5. 删除上下文中挂载的方法
delete context[funKey];
// 6. 返回执行结果
return result;
}
// apply 方法实现: 重点是将当前方法(this) 挂载到 context 后执行, 和 call 基本一样, 只是处理参数部分有点区别
// 1. 在函数原型(Function.prototype)上挂载方法
Function.prototype.myApply = function(context, args) {
// 2. 处理函数执行上下文, 判断传入的上下文是否是一个对象, 如果不是默认为 window
context = (context === undefined || context === null) ? window : Object(context)
// 3. 将当前方法挂载到上下文中, 使用 symbol 作为 key 避免属性冲突
const funKey = Symbol();
context[funKey] = this;
// 4. 处理参数并执行上下文中挂载的方法
const result = context[funKey](Array.isArray(args) ? ...args : void 0);;
// 5. 删除上下文中挂载的方法
delete context[funKey];
// 6. 返回执行结果
return result;
}
// bind 方法实现: 生成函数, 函数内调用 apply
// 1. 在函数原型(Function.prototype)上挂载方法
Function.prototype.myBind = function(context, ...args) {
// 2. 声明变量, 存储原方法
const self = this;
// 3. 返回新方法, 新方法内使用 apply 调用了原方法, 改变了 this 指向、修改参数
return function(...args2) {
return self.apply(context, args.concat(args2));
}
}
未完待续, 敬请期待!!!!!