前端面试题整理
JS
call, apply, bind区别
当函数需要传递多个变量时, apply 可以接受一个数组作为参数输入, call 则是接受一系列的单独变量。
Bind和call很相似,第一个参数是this的指向,从第二个参数开始是接收的参数列表。
bind返回对应函数, 便于稍后调用; apply, call则是立即调用。
var是否可以省略
一般情况下,是可以省略var的,但有两点值得注意:
var a=1
与a=1
,这两条语句一般情况下作用是一样的。但是前者不能用delete删除。不过,绝大多数情况下,这种差异是可以忽略的。- 在函数内部,如果没有用
var
进行申明,则创建的变量是全局变量,而不是局部变量了。
所以,建议变量申明加上var
关键字。
变量提升
JavaScript引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升(hoisting)。
示例:
1 | console.log(a); |
2 | var a =1; |
以上语句并不会报错,只是提示undefined
。实际运行过程:
1 | var a; |
2 | console.log(a); |
3 | a =1; |
表示变量a已声明,但还未赋值。但是变量提升只对var命令声明的变量有效,如果一个变量不是用var命令声明的,就不会发生变量提升。
1 | console.log(aa); |
2 | aa =1; |
以上代码将会报错:ReferenceError: aa is not defined
。
与普通变量一样,js里的function也可看做变量,也存在变量提升情况:
1 | a(); |
2 | |
3 | function a(){ |
4 | console.log(1); |
5 | }; |
表面上,上面代码好像在声明之前就调用了函数a。但是实际上,由于“变量提升”,函数a定义部分被提升到了代码头部,也就是在调用之前已经声明了。但是,如果采用赋值语句定义函数,JavaScript就会报错:
1 | a(); |
2 | |
3 | var a = function(){ |
4 | console.log(1); |
5 | }; |
6 | |
7 | // TypeError: a is not a function |
因为,实际运行过程:
1 | var a; |
2 | a(); |
3 | |
4 | a = function(){ |
5 | console.log(1); |
6 | }; |
这时候a是个变量,并非function。
this的作用域
普通函数和”this”
this 关键字的价值完全取决于它的函数(或方法)是如何被调用的。this 可以是以下任何内容:
- 新的对象
如果函数使用new
被调用:
1 | const mySundae = new Sundae('Chocolate', ['Sprinkles', 'Hot Fudge']); |
在上述代码中,Sundae 这个构造函数内的 this 的值是新的对象,因为它使用 new 被调用。
- 指定的对象
如果函数使用 call/apply 被调用:
1 | const result = obj1.printName.call(obj2); |
在上述代码中,printName() 中的 this 的值将指的是 obj2,因为 call() 的第一个参数明确设定 this 指代的是什么。
- 上下文对象
如果函数是对象方法:
1 | data.teleport(); |
在上述代码中,teleport() 中的 this 的值将指代 data。
- 全局对象或 undefined
如果函数被调用时没有上下文:
1 | teleport(); |
在上述代码中,teleport() 中的 this 的值是全局对象,如果在严格模式下,是 undefined。
箭头函数和”this”
对于普通函数,this 的值基于函数如何被调用。对于箭头函数,this 的值基于函数周围的上下文。换句话说,箭头函数内的,this 的值与函数外面的 this 的值一样。
我们先看一个普通函数中的 this 示例,再看一个箭头函数是如何使用 this 的。
1 | // constructor |
2 | function IceCream() { |
3 | this.scoops = 0; |
4 | } |
5 | |
6 | // adds scoop to ice cream |
7 | IceCream.prototype.addScoop = function() { |
8 | setTimeout(function() { |
9 | this.scoops++; |
10 | console.log('scoop added!'); |
11 | }, 500); |
12 | }; |
13 | |
14 | const dessert = new IceCream(); |
15 | dessert.addScoop(); |
Prints: scoop added!
运行上述代码后,你会认为半毫秒之后,dessert.scoops 会是1。但并非这样:
1 | console.log(dessert.scoops); |
Prints: 0
能说说原因吗?
传递给 setTimeout() 的函数被调用时没用到 new、call() 或 apply(),也没用到上下文对象。意味着函数内的 this 的值是全局对象,不是 dessert 对象。实际上发生的情况是,创建了新的 scoops 变量(默认值为 undefined),然后递增(undefined + 1 结果为 NaN):
1 | console.log(scoops); |
Prints: NaN
解决这个问题的方法之一是使用闭包:
1 | // constructor |
2 | function IceCream() { |
3 | this.scoops = 0; |
4 | } |
5 | |
6 | // adds scoop to ice cream |
7 | IceCream.prototype.addScoop = function() { |
8 | const cone = this; // sets `this` to the `cone` variable |
9 | setTimeout(function() { |
10 | cone.scoops++; // references the `cone` variable |
11 | console.log('scoop added!'); |
12 | }, 0.5); |
13 | }; |
14 | |
15 | const dessert = new IceCream(); |
16 | dessert.addScoop(); |
上述代码将可行,因为它没有在函数内使用 this,而是将 cone 变量设为 this,然后当函数被调用时查找 cone 变量。这样可行,因为使用了函数外面的 this 值。如果现在查看甜点中的勺子数量,正确值将为 1:
1 | console.log(dessert.scoops); |
Prints: 1
这正是箭头函数的作用,我们将传递给 setTimeout() 的函数替换为箭头函数:
1 | // constructor |
2 | function IceCream() { |
3 | this.scoops = 0; |
4 | } |
5 | |
6 | // adds scoop to ice cream |
7 | IceCream.prototype.addScoop = function() { |
8 | setTimeout(() => { // an arrow function is passed to setTimeout |
9 | this.scoops++; |
10 | console.log('scoop added!'); |
11 | }, 0.5); |
12 | }; |
13 | |
14 | const dessert = new IceCream(); |
15 | dessert.addScoop(); |
因为箭头函数从周围上下文继承了 this 值,所以这段代码可行!
1 | console.log(dessert.scoops); |
Prints: 1
当 addScoop() 被调用时,addScoop() 中的 this 的值指的是 dessert。因为箭头函数被传递给 setTimeout(),它使用周围上下文判断它里面的 this 指的是什么。因为箭头函数外面的 this 指的是 dessert,所以箭头函数里面的 this 的值也将是 dessert。
如果我们将 addScoop() 方法改为箭头函数,你认为会发生什么?
1 | // constructor |
2 | function IceCream() { |
3 | this.scoops = 0; |
4 | } |
5 | |
6 | // adds scoop to ice cream |
7 | IceCream.prototype.addScoop = () => { // addScoop is now an arrow function |
8 | setTimeout(() => { |
9 | this.scoops++; |
10 | console.log('scoop added!'); |
11 | }, 0.5); |
12 | }; |
13 | |
14 | const dessert = new IceCream(); |
15 | dessert.addScoop(); |
这段代码因为同一原因而不起作用,即箭头函数从周围上下文中继承了 this 值。在 addScoop() 方法外面,this 的值是全局对象。因此如果 addScoop() 是箭头函数,addScoop() 中的 this 的值是全局对象。这样的话,传递给 setTimeout() 的函数中的 this 的值也设为了该全局对象!
ES6新特性
- Let and Const
- 模板字面量
- 结构
- 对象字面量简写法
- 迭代 For…of循环
- 展开…运算符
- …剩余参数
- 箭头函数
- 默认函数参数
- 默认值和解构
- 类
- super and extends
- Symbol
- Set
- WeakSet
- Map
- WeakMap
- Promise
- Proxy
- 生成器和迭代器
- Polyfill
- 转译 Babel
对象的拷贝
浅拷贝: 以赋值的形式拷贝引用对象,仍指向同一个地址,修改时原对象也会受到影响
Object.assign
- 展开运算符(…)
深拷贝: 完全拷贝一个新对象,修改时原对象不再受到任何影响
JSON.parse(JSON.stringify(obj))
1
2
: 性能最快
3
4
- 具有循环引用的对象时,报错
5
- 当值为函数、`undefined`、或`symbol`时,无法拷贝
6
7
- 递归进行逐一赋值
8
9
10
11
### 代码的复用
12
13
当你发现任何代码开始写第二遍时,就要开始考虑如何复用。一般有以下的方式:
14
15
- 函数封装
16
- 继承
17
- 复制`extend`
18
- 混入`mixin`
19
- 借用`apply/call`
20
21
22
23
### 原型链
24
25
**原型链是由原型对象组成**,每个对象都有 `__proto__` 属性,指向了创建该对象的构造函数的原型,`__proto__` 将对象连接起来组成了原型链。是一个用来**实现继承和共享属性**的有限的对象链。
26
27
- **属性查找机制**: 当查找对象的属性时,如果实例对象自身不存在该属性,则沿着原型链往上一级查找,找到时则输出,不存在时,则继续沿着原型链往上一级查找,直至最顶级的原型对象`Object.prototype`,如还是没找到,则输出`undefined`;
28
- **属性修改机制**: 只会修改实例对象本身的属性,如果不存在,则进行添加该属性,如果需要修改原型的属性时,则可以用: `b.prototype.x = 2`;但是这样会造成所有继承于该对象的实例的属性发生改变。
29
30
### 闭包
31
32
闭包属于一种特殊的作用域,称为 **静态作用域**。它的定义可以理解为: **父函数被销毁** 的情况下,返回出的子函数的`[[scope]]`中仍然保留着父级的单变量对象和作用域链,因此可以继续访问到父级的变量对象,这样的函数称为闭包。
33
34
- 闭包会产生一个很经典的问题:
35
- 多个子函数的`[[scope]]`都是同时指向父级,是完全共享的。因此当父级的变量对象被修改时,所有子函数都受到影响。
36
- 解决:
37
- 变量可以通过 **函数参数的形式** 传入,避免使用默认的`[[scope]]`向上查找
38
- 使用`setTimeout`包裹,通过第三个参数传入
39
- 使用 **块级作用域**,让变量成为自己上下文的属性,避免共享
40
41
42
43
### 防抖与节流
44
45
防抖与节流函数是一种最常用的 **高频触发优化方式**,能对性能有较大的帮助。
46
47
- **防抖 (debounce)**: 将多次高频操作优化为只在最后一次执行,通常使用的场景是:用户输入,只需再输入完成后做一次输入校验即可。
48
49
```js
50
function debounce(fn, wait, immediate) {
51
let timer = null
52
53
return function() {
54
let args = arguments
55
let context = this
56
57
if (immediate && !timer) {
58
fn.apply(context, args)
59
}
60
61
if (timer) clearTimeout(timer)
62
timer = setTimeout(() => {
63
fn.apply(context, args)
64
}, wait)
65
}
66
}
节流(throttle): 每隔一段时间后执行一次,也就是降低频率,将高频操作优化成低频操作,通常使用场景: 滚动条事件 或者 resize 事件,通常每隔 100~500 ms执行一次即可。
1 | function throttle(fn, wait, immediate) { |
2 | let timer = null |
3 | let callNow = immediate |
4 | |
5 | return function() { |
6 | let context = this, |
7 | args = arguments |
8 | |
9 | if (callNow) { |
10 | fn.apply(context, args) |
11 | callNow = false |
12 | } |
13 | |
14 | if (!timer) { |
15 | timer = setTimeout(() => { |
16 | fn.apply(context, args) |
17 | timer = null |
18 | }, wait) |
19 | } |
20 | } |
21 | } |
浏览器
跨标签页通讯
不同标签页间的通讯,本质原理就是去运用一些可以 共享的中间介质,因此比较常用的有以下方法:
- 通过父页面
window.open()
和子页面postMessage
- 异步下,通过
window.open('about: blank')
和tab.location.href = '*'
- 异步下,通过
- 设置同域下共享的
localStorage
与监听window.onstorage
- 重复写入相同的值无法触发
- 会受到浏览器隐身模式等的限制
- 设置共享
cookie
与不断轮询脏检查(setInterval
) - 借助服务端或者中间层实现
Web Worker
内存泄露
- 意外的全局变量: 无法被回收
- 定时器: 未被正确关闭,导致所引用的外部变量无法被释放
- 事件监听: 没有正确销毁 (低版本浏览器可能出现)
- 闭包: 会导致父级中的变量无法被释放
- dom 引用: dom 元素被删除时,内存中的引用未被正确清空
可用 chrome 中的 timeline 进行内存标记,可视化查看内存的变化情况,找出异常点。
服务端
Web Socket
跨域
- JSONP: 利用
<script>
标签不受跨域限制的特点,缺点是只能支持 get 请求
1 | function jsonp(url, jsonpCallback, success) { |
2 | const script = document.createElement('script') |
3 | script.src = url |
4 | script.async = true |
5 | script.type = 'text/javascript' |
6 | window[jsonpCallback] = function(data) { |
7 | success && success(data) |
8 | } |
9 | document.body.appendChild(script) |
10 | } |
11 | 复制代码 |
- 设置 CORS: Access-Control-Allow-Origin:*
- postMessage
安全
- XSS攻击: 注入恶意代码
- cookie 设置 httpOnly
- 转义页面上的输入内容和输出内容
- CSRF: 跨站请求伪造,防护:
- get 不修改数据
- 不被第三方网站访问到用户的 cookie
- 设置白名单,不被第三方网站请求
- 请求校验
算法
快速排序
1 | function quickSort(arr){ |
2 | if (arr.length<=1){ |
3 | return arr; |
4 | } |
5 | var pivotIndex = 0, |
6 | pivort = arr.splice(pivortIndex, 1)[0]; |
7 | var left = [], |
8 | right = []; |
9 | for (var i = 1, length = arr.length; i < length; i++) { |
10 | if (arr[i] < pivort) { |
11 | left.push(arr[i]); |
12 | }else if (arr[i] > = pivort) { |
13 | right.push(arr[i]); |
14 | } |
15 | } |
16 | return quickSort(left).concat([pivort], quickSort(right)); |
17 | } |
深克隆
1 | function deepClone (test) { |
2 | if (Array.isArray(test)) { |
3 | var arr = []; |
4 | for (var i = 0, length = test.length; i < length; i++) { |
5 | arr.push(clone(arr[i])); |
6 | } |
7 | }else if (typeOf test === "object") { |
8 | var obj = {}; |
9 | for (var j in test) { |
10 | obj[j] = clone(test[j]); |
11 | } |
12 | return obj; |
13 | }else { |
14 | return test; |
15 | } |
16 | } |
参考资料