JS 基础面试题
搞懂 this
要明白 this
的指向,最有用的一句话:this
永远指向最后调用它的对象
最基础的例子:
1 | function foo() { |
用 var
来声明的变量属于全局变量,所以执行完 var a = 1
之后,实际上就是 window.a = 1
,window
在这里是一个全局的对象。
第一次输出,由于我们是直接调用了函数
foo()
,根据我们的原则this
永远指向最后调用它的对象,因此很明显,此时this
指向的就是windows
,自然控制台打印出来的就是1
。第二次,我们新建了一个对象
obj
,foo
属于这个对象的一个方法,所以最后调用foo()
函数的其实是这个对象,所以this
指向的是这个obj
,打印出来的就是2
。第三次,我们
new
了一个foo()
函数,this
被永远绑定在c
的上面,由于在新建c
的时候,实际上它并没有叫做a
的属性,所以最后打印出来的是undefined
。
再来看这个例子:
1 | var name = 'ethan' |
这里的 innerFunction()
和我们上面那个例子中的第一次输出类似,都是直接调用了函数,没有挂载在任何对象上,因此它的 this
就是指向的是 windows
。
下面这个例子可以帮你巩固这个知识点:
1 | var name = "ethan"; |
可以简单地理解为,匿名函数的 this
永远指向 windows
,在我们调用对象 a
的 func2
方法的时候, 我们使用 setTimeOut()
来直接调用声明的匿名函数,此时由于 windows
并不具备 func1()
函数,所以会报错。
针对箭头函数而言,它的 this
永远绑定函数定义时候的 this
,而不是执行时候,也可以理解为箭头函数的 this
指向的是包裹箭头函数的第一个普通函数中的 this
,如果找不到就是 undefined
。
具体例子如下:
1 | var name = "ethan"; |
改变 this 指向
使用 ES6 的语法,箭头函数:始终指向定义时的
this
,而不是执行的时候。1
2
3
4
5
6
7
8
9
10
11
12var name = 'ethan';
var a = {
name: 'echo',
func1: function() {
console.log(this.name)
}
func2: function() {
setTimeout(() =>{
this.func1();
}, 100)
}
}不使用 ES6 的语法,使用
_this = this
,来保存this
指针。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18var name = "ethan";
var a = {
name: "echo",
func1: function () {
console.log(this.name)
},
func2: function () {
var _this = this;
setTimeout(function () {
_this.func1()
}, 100)
}
};
a.func2() // echo使用
apply, call, bind
1
2
3
4
5
6
7
8
9setTimeout(function () {
this.func1()
}.apply(a), 100)
setTimeout(function () {
this.func1()
}.call(a), 100)
setTimeout(function () {
this.func1()
}.bind(a)(), 100)apply
和call
比较类似,它们都是改变this
指向并且调用函数,bind
跟它们的区别就体现在它只改变绑定的this
,不会直接调用。apply
和call
的区别体现在函数有多参数的情况下,前者是接收多参数的数组,后者是直接将多个参数平铺在apply()
内部:1
2
3
4
5
6
7
8
9
10const a = {
name: 'ethan',
fn: function (a, b) {
console.log(a + b)
}
}
const b = a.fn;
b.apply(a, [1, 2]) // 3
b.call(a, 3, 4) // 7
判断 this
指向的图片流程总结:
==
关键记忆点:字符串转数字,布尔转数字,对象转基本类型
闭包
MDN 上对闭包的定义:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。
JS 中闭包的意义就是让我们可以间接地访问函数内部的变量。
经典例子:
1 | function a() { |
闭包是由函数以及声明该函数的词法环境组合而成,这个环境包含了该闭包创建时作用域内的任何局部变量。在上面这个例子中,实际上 c
是执行 a
时创建的 b
函数实例的引用。b
的实例维持了一个对它的词法环境的引用,因此调用 c
的时候仍然有 name
这个变量。
闭包的应用:
1 | function powerX(x) { |
这个例子相当于一个工厂函数,封装了一个可以求 X
次方的函数。
经典面试题:由于 setTimeOut
是一个异步函数,所以会先把循环执行完毕,当前输出结果是一堆 6
,如何输出 1
到 5
?
1 | for (var i = 1; i <= 5; i++) { |
解法1:闭包的应用
1 | for (var i = 1; i <= 5; i++) { |
解法2:使用 setTimeOut
的第三个参数,这个参数会作为回调函数的参数传入
1 | for (var i = 1; i <= 5; i++) { |
解法3:推荐,用 ES6 中的 let
来解决问题
1 | for (let i = 1; i <= 5; i++) { |
深浅拷贝
对于基本类型而言,例如 number, boolean, string
,它们的拷贝不分深浅,都是将数值直接进行拷贝。
例如 let a = 3; let b = a
,实际上就是直接赋值,不涉及任何拷贝。
同样是直接赋值的案例:
1 | const a = [1, 2, 3] |
由于 a
变量是一个对象,我们直接引用的其实是对象的地址,因此 b = a
实际上就是让 b
指向 a
指向的地方。它们俩指的是同样一块内存空间,所以我们对其中任何一个的改变都影响到了对方。
数组的浅拷贝:
1 | const a = [1, 2, 3] |
对象的浅拷贝:
1 | const a = { name: 'ethan', age: 21 } |
浅拷贝的问题体现在它只能解决第一层数据的复制,当出现引用的引用的时候,它就没有效果了。
1 | const a = { name: 'ethan', age: 21, games: ['LOL'] } |
为了解决这个问题,我们需要深克隆,来实现引用类型内部的引用类型数值的拷贝。
简易版实现:
1 | const a = { name: 'ethan', age: 21, games: ['LOL'] } |
简易版的问题体现在:
- 会忽略
undefined
- 会忽略
symbol
- 无法拷贝函数
- 无法拷贝循环引用的对象
手写深拷贝:
1 | const deepClone = (target) => { |
当遇到需要拷贝的内容是对象且非空的时候,遍历目标对象的所有属性,进行递归的复制。
当然这个函数还是有很多缺陷:循环引用,
Data, Set, Map
等特殊对象的复制,函数拷贝。完整版的可以参考 https://juejin.cn/post/6844903986479251464#heading-62,由于挖的比较深,我这儿就先不探究了。
实现继承
组合继承方式:
1 | function Parent(val) { |
子类的构造函数通过 Parent.call(this)
继承了父类的属性,把子类的原型改成 new Parent()
来继承父类的函数。
该种继承方式的优点在于构造函数可以传参,不会和父类共享引用属性,可以继承父类的函数;缺点在于继承父类的函数的时候实际上调用了父类的构造函数,因此造成了子类的原型上存在了不需要的属性。
**寄生组合继承:**对组合继承的优化
1 | function Parent(val) { |
class
关键词实现继承,属于 ES6 中提供的语法糖
1 | class Parent { |
AJAX
AJAX(Asynchronous JavaScript And XML):异步的 JS 和 XML(可扩展标记语言)
可以实现在不刷新的情况下,更新页面上的数据,提高用户体验。
AJAX 经典四步:
- 创建
XMLHttpRequest
的一个实例 - 调用实例的
open
方法 - 指定状态变化事件处理函数
- 调用
send
方法发送请求
1 | var xhr = new XMLHttpRequest(); |
Promise
3.17 MS 二面问到了,面完赶紧补课
Promise
是 ES6 提出用来处理异步问题的一个方案。
Promise
的三个状态:pending(等待), resolved(已成功), rejected(已失败)
。它的优点在于将异步操作用同步方式的流程表达出来,使用链式的写法使得代码更加优雅。
基本用法:
1 | // 1. 封装 model 层的接口 |
原型和原型链
先记住原型链,再尝试理解原型。
一开始我们先看一张图:
再根据代码来理解:
1 | function Person() { |
构造函数的 prototype
属性:
实例对象的 __proto__
属性:
两者指向一个地方:person.__proto__ === Person.prototype
同时,原型的 constructor
属性指向的是构造函数:Person()
ES5 中获取对象的原型的方法是
Object.getPrototypeOf(person)
我们能更新当前的关系图:
再来继续探究原型是什么东西,我们可以看到之前打印的原型 Person.prototype
或者说 person.__proto__
,它是一个对象,我们尝试获取它的原型(即原型的原型)
Object()
是 Object
的构造函数,而我们之前 Person
的原型是一个 Object
的实例,所以我们可以得到 Person.prototype.__proto__ === Object.prototype
。
Object
的原型的原型为null
最终的原型链图就张这样(图中的蓝色这根线):
那父类和子类的继承关系和这个原型又有什么关系呢?
1 | class F { |
- 子类的
__proto__
属性,表示构造函数的继承,指向父类;因为作为一个对象,子类的原型是父亲 - 子类
prototype
属性的__proto__
属性,表示方法的继承,总是指向父类的prototype属性;因为作为一个构造函数,子类的原型对象是父类的原型对象的实例
走完这个流程之后,我们再来探究什么是 prototype
,什么是 __proto__
__proto__
,是对象的一个属性,它的值就是它所对应的原型对象
prototype
,是函数的一个属性,它的值是一个有 constructor
属性的对象。当我们把这个函数作为构造函数调用的时候,JS 就会帮我们创建该构造函数的实例,实例继承该构造函数的 prototype
的所有属性和方法(实例会把自己的 __proto__
指向构造函数的 prototype
)。
实际上我对原型链的理解还是很模糊,建议直接阅读 MDN 相关文档。