JavaScript面经

js判断类型的方法

1
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
1. typeof
// 只能判断原始类型(基本数据类型)
// 判断 null 除外

typeof null // object
typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'

2. Object.prototype.toString()
Object.prototype.toString.call({}) // '[object Object]'

3. instanceof
// 适用于判断对象类型(引用数据类型)

const Person = function() {}
const p1 = new Person()
p1 instanceof Person // true

var arr = []
arr instanceof Array // true

// 对于原始类型来说,你想直接通过 instanceof 来判断类型是不行的,当然我们还是有办法让 instanceof 判断原始类型的

class PrimitiveString {
static [Symbol.hasInstance](x) {
return typeof x === 'string'
}
}
console.log('hello world' instanceof PrimitiveString) // true

== 和 === 有什么区别?

== 如果对比双方的类型不一样的话,就会进行类型转换

=== 判断两者类型和值是否都相同

== 判断流程

  1. 首先会判断两者类型是否相同。相同的话就是比大小了
  2. 类型不相同的话,那么就会进行类型转换
  3. 会先判断是否在对比 null 和 undefined,是的话就会返回 true
  4. 判断两者类型是否为 string 和 number,是的话就会将字符串转换为 number
  5. 判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断
  6. 判断其中一方是否为 object 且另一方为 string、number 或者 symbol,就调用 object 的valueOf()方法,用得到的基本类型值按照前面的规则进行比较,如果 object 没有valueOf()方法,则调用 toString()
  7. 如果有一个操作数是NaN,则相等操作符返回 false
1
2
3
'1' == { name: 'yck' }
↓
'1' == '[object Object]'

面试题:[] == ![]

1
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
1. [] == ![]    // true

根据运算符优先级 ,! 的优先级是大于 == 的,所以先会执行 ![]
!可将变量转换成boolean类型,null、undefined、NaN以及空字符串('')取反都为true,其余都为false。
!null == true
!undefined == true
!NaN == true
!'' == true

![] == false
!{} == false

总结:
[].toString() -> '' (返回的是空字符串),也就是 [] == 0 相当于 '' == 0

[] == ! [] -> [] == false -> [] == 0 -> '' == 0 -> 0 == 0 -> true

同理:{} == !{} // true
{}.toString() -> NaN(返回的是NaN)

{} == ! {} -> {} == false -> {} == 0 -> NaN == 0 -> false

2. 0 == +[]
// + 操作符是转为Number类型

0 == +[] -> 0 == 0 -> true

0 == +{} -> 0 == NaN -> false

call和apply

一、call 和 apply 都是为了改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内部 this 的指向;

apply和call都能继承另外一个对象的方法和属性;

1
2
3
4
5
6
7
8
9
10
11
12
13

var foo = {
value: 1
};

function bar() {
console.log(this.value);
}

bar.call(foo); // 1

1. call 改变了 this 的指向,指向到 foo
2. bar 函数执行了

二、对于 apply、call 二者而言,作用完全一样,只是接受参数的方式不太一样。例如,有一个函数定义如下:

1
2
3
4
5
6
7
8
var func = function(arg1, arg2) {

};

就可以通过如下方式来调用:

func.call(this, arg1, arg2);
func.apply(this, [arg1, arg2]);

其中 this 是你想指定的上下文,他可以是任何一个 JavaScript 对象(JavaScript 中一切皆对象),call 需要把参数按顺序传递进去,而 apply 则是把参数放在数组里;

模拟实现call和apply

可以从以下几点来考虑如何实现

  • 不传入第一个参数,那么默认为 window
  • 改变了 this 指向,让新的对象可以执行该函数。那么思路是否可以变成给新的对象添加一个函数,然后在执行完以后删除?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// call
Function.prototype.myCall = function (context) {
var context = context || window
// 给 context 添加一个属性
// getValue.call(a, 'yilidan', '24') => a.fn = getValue
context.fn = this
// 将 context 后面的参数取出来
var args = [...arguments].slice(1)
// getValue.call(a, 'yilidan', '24') => a.fn('yilidan', '24')
var result = context.fn(...args)
// 删除 fn
delete context.fn
return result
}

以上就是 call 的思路,apply 的实现也类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// apply
Function.prototype.myApply = function (context) {
var context = context || window
context.fn = this

var result
// 需要判断是否存储第二个参数
// 如果存在,就将第二个参数展开
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}

delete context.fn
return result
}

bind 和其他两个方法作用也是一致的,只是该方法会返回一个函数。并且我们可以通过 bind 实现柯里化。

同样的,也来模拟实现下 bind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// bind
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
var _this = this
var args = [...arguments].slice(1)
// 返回一个函数
return function F() {
// 因为返回了一个函数,我们可以 new F(),所以需要判断
if (this instanceof F) {
return new _this(...args, ...arguments)
}
return _this.apply(context, args.concat(...arguments))
}
}

对闭包的理解

红宝书(p178)上对于闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数。

MDN 对闭包的定义为:闭包是指那些能够访问自由变量的函数。 (其中自由变量,指在函数中使用的,但既不是函数参数arguments也不是函数的局部变量的变量,其实就是另外一个函数作用域中的变量。)

口述理解:函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。

闭包的表现形式

1
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

1. 返回一个函数
function f1() {
var a = 2
function f2() {
console.log(a); // 2
}
return f2;
}
var x = f1();
x();

2. 作为函数参数传递
var a = 1;
function foo(){
var a = 2;
function baz(){
console.log(a);
}
bar(baz);
}
function bar(fn){
// 这就是闭包
fn();
}
// 输出2,而不是1
foo();

3. 在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。

以下的闭包保存的仅仅是window和当前作用域。

// 定时器
setTimeout(function timeHandler(){
console.log('111');
},100)

// 事件监听
$('#app').click(function(){
console.log('DOM Listener');
})

4. IIFE(立即执行函数表达式)创建闭包, 保存了全局作用域window和当前函数的作用域,因此可以全局的变量。

var a = 2;
(function IIFE(){
// 输出2
console.log(a);
})();

如何解决下面的循环输出问题?

1
2
3
4
5
6
7

for(var i = 1; i <= 5; i ++){
setTimeout(function timer(){
console.log(i)
}, 0)
}
输出:6,6,6,6,6

为什么会全部输出6?如何改进,让它输出1,2,3,4,5?(方法越多越好)

因为setTimeout为宏任务,由于JS中单线程eventLoop机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后setTimeout中的回调才依次执行,但输出i的时候当前作用域没有,往上一级再找,发现了i,此时循环已经结束,i变成了6。因此会全部输出6。

解决方法:

1
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
33
34

1. 利用IIFE(立即执行函数表达式)当每次for循环时,把此时的i变量传递到定时器中;

for(var i = 1;i <= 5;i++){
(function(j){
setTimeout(function timer(){
console.log(j)
}, 0)
})(i)
}

// 1,2,3,4,5

2. 给定时器传入第三个参数, 作为timer函数的第一个函数参数;

for(var i=1;i<=5;i++){
setTimeout(function timer(j){
console.log(j)
}, 0, i)
}

// 1,2,3,4,5

3. 使用ES6中的let

for(let i = 1; i <= 5; i++){
setTimeout(function timer(){
console.log(i)
},0)
}

// 1,2,3,4,5

let使JS发生革命性的变化,让JS有函数作用域变为了块级作用域,用let后作用域链不复存在。代码的作用域以块级为单位{}。

对原型链的理解

原型对象和构造函数有何关系

在JavaScript中,每当定义一个函数数据类型(普通函数、类)时候,都会天生自带一个prototype属性,这个属性指向函数的原型对象。

当函数经过new调用时,这个函数就成为了构造函数,返回一个全新的实例对象,这个实例对象有一个proto属性,指向构造函数的原型对象。

原型链的描述

JavaScript通过proto指向父类对象,直到指向Object对象为止,这样就行程一个原型指向的链条,即原型链;

  • 对象的 hasOwnProperty() 来检查对象自身中是否含有该属性
  • 使用 in 检查对象中是否含有某个属性时,如果对象中没有但是原型链中有,也会返回 true

JS如何实现继承

组合继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

function Parent(value) {
this.val = value
}
Parent.prototype.getValue = function() {
console.log(this.val)
}
function Child(value) {
Parent.call(this, value)
}
Child.prototype = new Parent()

const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true

寄生组合继承(推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 写法一:
function Parent(value) {
this.val = value
}
Parent.prototype.getValue = function() {
console.log(this.val)
}
function Child(value) {
Parent.call(this, value)
}

Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child,
enumerable: false,
writable: true,
configurable: true
}
})

const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
1
2
3
4
5
6
7
8
9
10
11
12
13
// 写法二:
function Parent5 () {
this.name = 'parent5';
this.play = [1, 2, 3];
}

function Child5() {
Parent5.call(this);
this.type = 'child5';
}

Child5.prototype = Object.create(Parent5.prototype);
Child5.prototype.constructor = Child5;

ES6 extends关键字继承(推荐)

1
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

class Person {
constructor(name) {
this.name = name
}
printName() {
console.log('父类')
}
commonFunction() {
console.log('公共方法')
}
}
// 继承父类
class Student extends Person{
constructor(name, score) {
super(name)
this.score = score
}
printScore() {
console.log('子类')
}
}

let p = new Person('小红')
let s = new Student('小明', 100)
console.log(p.commonMethods===s.commonMethods) // true

for of 与 for in 的区别

for…in循环出的是key,for…of循环出的是value

for in

1
2
3
for(let index in aArray){
console.log(`${aArray[index]}`)
}

for of

1
2
3
for(var value of aArray){
console.log(value)
}

new 实现过程

  1. 新生成了一个对象
  2. 链接到原型
  3. 绑定this
  4. 返回新对象
1
2
3
4
5
6
7
8
9
10
11
12
function create() {
// 创建一个空的对象
let obj = new Object()
// 获得构造函数
let Con = [].shift.call(arguments)
// 链接到原型
obj.__proto__ = Con.prototype
// 绑定 this,执行构造函数
let result = Con.apply(obj, arguments)
// 确保 new 出来的是个对象
return typeof result === 'object' ? result : obj
}

指定随机数范围(整数)

1
2
3
4
function sum(m,n) {
return Math.floor(Math.random()*(m - n) + n)
}
sum(1, 100)

深拷贝与浅拷贝

产生的原因

1
2
3
4
5
6
let a = {
age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2

从上述例子中我们可以发现,如果给一个变量赋值一个对象,那么两者的值会是同一个引用,其中一方改变,另一方也会相应改变。

通常在开发中我们不希望出现这样的问题,我们可以使用浅拷贝来解决这个问题。

浅拷贝使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
方法一:Object.assign

let a = {
age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1

方法二:展开运算符 ...

let a = {
age: 1
}
let b = {...a}
a.age = 2
console.log(b.age) // 1

通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就需要使用到深拷贝了

1
2
3
4
5
6
7
8
9
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = {...a}
a.jobs.first = 'native'
console.log(b.jobs.first) // native

浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到刚开始的话题了,两者享有相同的引用。要解决这个问题,我们需要引入深拷贝。

深拷贝

这个问题通常可以通过 JSON.parse(JSON.stringify(object)) 来解决。

1
2
3
4
5
6
7
8
9
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE

但是该方法也是有局限性的:

  • 会忽略 undefined
  • 会忽略 symbol
  • 不能序列化函数
  • 不能解决循环引用的对象

在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题,并且该函数是内置函数中处理深拷贝性能最快的。当然如果你的数据中含有以上三种情况下,可以使用 lodash 的深拷贝函数。

this的理解

this 是很多人会混淆的概念,但是其实他一点都不难,你只需要记住几个规则就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function foo() {
console.log(this.a)
}
var a = 1
foo()

var obj = {
a: 2,
foo: foo
}
obj.foo()

// 以上两者情况 `this` 只依赖于调用函数前的对象,优先级是第二个情况大于第一个情况

// 以下情况是优先级最高的,`this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向
var c = new foo()
c.a = 3
console.log(c.a)

// 还有种就是利用 call,apply,bind 改变 this,这个优先级仅次于 new

以上几种情况明白了,很多代码中的 this 应该就没什么问题了,下面让我们看看箭头函数中的 this

1
2
3
4
5
6
7
8
function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())

箭头函数其实是没有 this 的,这个函数中的 this 只取决于他外面的第一个不是箭头函数的函数的 this。在这个例子中,因为调用 a 符合前面代码中的第一个情况,所以 this 是 window。并且 this 一旦绑定了上下文,就不会被任何代码改变。

Vue的生命周期

1
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
new Vue()
// 新建Vue实例

beforeCreate
// 是获取不到 props 或者 data 中的数据的,因为这些数据的初始化都在 initState 中。

created
// 可以访问到之前不能访问到的数据

beforeMount
// 开始创建 VDOM (Virtual DOM)

mounted
// 将 VDOM 渲染为真实 DOM 并且渲染数据

beforeUpdate
// 在数据更新前调用

updated
// 在数据更新后调用

使用keep-alive的情况下
// 用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 actived 钩子函数。

beforeDestroy
// 销毁组件的钩子函数,适合移除事件、定时器等等

destroyed
// 销毁完毕

Vue中computed和watch的区别

computed 是计算属性,依赖其他属性计算值,并且 computed 的值有缓存,只有当计算值变化才会返回内容。

watch 监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作

一般来说需要依赖别的属性来动态获得值的时候可以使用 computed

对于监听到值的变化需要做一些复杂业务逻辑的情况可以使用 watch。

Vue中响应式的原理

Vue 内部使用了 Object.defineProperty() 来实现数据响应式,通过这个函数可以监听到 set 和 get 的事件。

发布-订阅模式

发布-订阅模式也叫做观察者模式。通过一对一或者一对多的依赖关系,当对象发生改变时,订阅方都会收到通知

发布订阅模式核心是 addEventListener

在 Vue 中,如何实现响应式也是使用了该模式。对于需要实现响应式的对象来说,在 get 的时候会进行依赖收集,当改变了对象的属性时,就会触发派发更新。

什么是BFC

块格式化上下文(Block Formatting Context,BFC) 是Web页面的可视化CSS渲染的一部分,是块盒子的布局过程发生的区域,也是浮动元素与其他元素交互的区域。

如何创建BFC

  • 根元素
  • 浮动元素(float 属性不为 none)
  • position 为 absolute 或 fixed
  • overflow 不为 visible 的块元素
  • display 为 inline-block, table-cell, table-caption