> For the complete documentation index, see [llms.txt](https://onenesss-organization.gitbook.io/jj-xiaoce/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://onenesss-organization.gitbook.io/jj-xiaoce/qian-duan-mian-shi-zhi-dao/04.-js-ji-chu-zhi-shi-dian-ji-chang-kao-mian-shi-ti-er.md).

# JS 基础知识点及常考面试题（二）

在这一章节中我们继续来了解 JS 的一些常考和容易混乱的基础知识点。

## 更新日志

* 重写闭包及深浅拷贝章节

## == vs ===

对于 `==` 来说，如果对比双方的类型**不一样**的话，就会进行**类型转换**，这也就用到了我们上一章节讲的内容。

当我们需要对比 `x` 和 `y` 是否相同时，就会进行如下判断流程：

1. 首先会判断两者类型是否**相同**。相同的话就是比大小了
2. 类型不相同的话，那么就会进行类型转换
3. 会先判断是否在对比 `null` 和 `undefined`，是的话就会返回 `true`
4. 判断两者类型是否为 `string` 和 `number`，是的话就会将字符串转换为 `number`

   ```js
   1 == '1'
         ↓
   1 ==  1
   ```
5. 判断其中一方是否为 `boolean`，是的话就会把 `boolean` 转为 `number` 再进重写进行判断流程

   ```js
   '1' == true
           ↓
   '1' ==  1
           ↓
    1  ==  1
   ```
6. 判断其中一方是否为 `object` 且另一方为 `string`、`number`，是的话就会把 `object` 转为原始类型再进行判断，也就是执行 `x.toString()` 及 `valueOf`，具体内容在上一章节已经聊过

   ```js
   '1' == { name: 'yck' }
           ↓
   '1' == '[object Object]'
   ```
7. 返回 `false`

如果你觉得记忆步骤太麻烦的话，我还提供了流程图供大家使用：

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d16297d5ef504fb793681925db2df865~tplv-k3u1fbpfcp-zoom-1.image)

以上内容大家也可以参考 [标准文档](https://www.ecma-international.org/ecma-262/5.1/#sec-11.9.1)。

最后对于 `===` 来说，判断简单多了，就是判断两者类型和值是否相同就行。

### 常见面试题

* `==` 和 `===` 操作符有什么区别
* `==` 操作符的类型转换规则

## 闭包

> 很多读者的闭包概念是完全错误的，如果你觉得你理解的闭包概念与我不同，那么请以我的内容为准。

首先闭包正确的定义是：**假如一个函数能访问外部的变量，那么就形成了一个闭包，而不是一定要返回一个函数**。这个定义很重要，下面的内容需要用到。

```js
let a = 1
// 产生闭包
function fn() {
  console.log(a);
}

function fn1() {
  let a = 1
  // 产生闭包
  return () => {
    console.log(a);
  }
}
const fn2 = fn1()
fn2()
```

其实到这里为止闭包的概念已经讲完了，没有别的东西了。接下来我们通过 Chrome 的开发工具来进一步了解闭包是如何存储外部变量的。

先来说下数据存放的正确规则是：局部、占用空间确定的数据，一般会存放在栈中，否则就在堆中（也有例外）。 那么接下来我们来验证这个说法：

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/57e6527dcc6e4ac1be201cc3a052be8e~tplv-k3u1fbpfcp-zoom-1.image)

上图中画红框的位置我们能看到一个内部的对象 `[[Scopes]]`，这个对象就是我们常说的作用域链。根据作用域链寻找的顺序，其中包含了闭包、全局对象。据此我们能通过闭包访问到本该销毁的变量，所以我会在上一章节中说原始数据一般存放在栈上。

另外最开始我们对于闭包的定位是：**假如一个函数能访问外部的变量，那么就形成了一个闭包**，因此接下来我们看看在全局下的表现是怎么样的。

```js
let a = 1
var b = 2
// 形成闭包
function fn() {
  console.log(a, b);
}
```

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2f00fc5ea99742b7963289728ee8f094~tplv-k3u1fbpfcp-zoom-1.image)

从上图我们能发现全局下声明的变量，如果是 var 的话就直接被挂到 `global` 上，如果是其他关键字声明的话就被挂到 `Script` 上。虽然这些数据同样还是存在 `[[Scopes]]` 上，但是全局变量在内存中是存放在静态区域的，因为全局变量无需进行垃圾回收。

最后总结一下原始类型存储位置：**局部变量被存储在栈上，全局变量存储在静态区域上，其它都存储在堆上。**

### 常见考点

闭包能考的很多，概念和笔试题都会考。

概念题就是考考闭包是什么了。

笔试题的话基本都会结合上异步，比如最常见的：

```js
for (var i = 0; i < 6; i++) {
  setTimeout(() => {
    console.log(i)
  })
}
```

这道题会问输出什么，有哪几种方式可以得到想要的答案？

## 深浅拷贝

在上一章节中，我们了解了对象类型在赋值的过程中其实是复制了地址，从而会导致改变了一方时其它也都被改变的情况。通常在开发中我们不希望出现这样的问题，此时我们可以使用浅拷贝来解决这个情况。

### 浅拷贝

首先可以通过 `Object.assign` 来解决这个问题，这个函数会拷贝所有的属性值到新的对象中。如果属性值是对象的话，拷贝的是地址。

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

另外我们还可以通过展开运算符 `...` 来实现浅拷贝：

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

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

```js
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))` 来解决，这个方式基本能解决大部分情况。

```js
let a = {
  age: 1,
  jobs: {
    first: 'FE'
  }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
```

当然了，这个方法是存在局限性的。

```js
let obj = {
  a: 1,
  b: {
    c: 2,
    d: 3,
  },
}
obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)
```

如果对象中存在循环引用，你会发现程序会报错：

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7ca679bf3b3b4ae59783a4b9298a1665~tplv-k3u1fbpfcp-zoom-1.image)

同时在遇到不支持的数据类型，比如函数、 `undefined` 或者 `symbol` 的时候，这些属性都会被忽略：

```js
let a = {
  age: undefined,
  sex: Symbol('male'),
  jobs: function() {},
  name: 'yck'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "yck"}
```

你会发现在上述代码中，`JSON.stringify(a)` 只保留了 `name` 属性。

以下是 JSON 支持的数据类型：

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/58d9559d8cbe4c13b94bc6fc2ec703de~tplv-k3u1fbpfcp-zoom-1.image)

如果你所需拷贝的对象含有内置类型并且不包含函数，可以使用 `MessageChannel`：

```js
function structuralClone(obj) {
  return new Promise(resolve => {
    const { port1, port2 } = new MessageChannel()
    port2.onmessage = ev => resolve(ev.data)
    port1.postMessage(obj)
  })
}

var obj = {
  a: 1,
  b: {
    c: 2
  }
}

obj.b.d = obj.b

// 注意该方法是异步的
// 可以处理 undefined 和循环引用对象
const test = async () => {
  const clone = await structuralClone(obj)
  console.log(clone)
}
test()
```

当然我们也可以自己实现一个深拷贝，同时这也是一个非常高频的手写题：

```js
// 利用 WeakMap 解决循环引用
let map = new WeakMap()
function deepClone(obj) {
  if (obj instanceof Object) {
    if (map.has(obj)) {
      return map.get(obj)
    }
    let newObj
    if (obj instanceof Array) {
      newObj = []     
    } else if (obj instanceof Function) {
      newObj = function() {
        return obj.apply(this, arguments)
      }
    } else if (obj instanceof RegExp) {
      // 拼接正则
      newobj = new RegExp(obj.source, obj.flags)
    } else if (obj instanceof Date) {
      newobj = new Date(obj)
    } else {
      newObj = {}
    }
    // 克隆一份对象出来
    let desc = Object.getOwnPropertyDescriptors(obj)
    let clone = Object.create(Object.getPrototypeOf(obj), desc)
    map.set(obj, clone)
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        newObj[key] = deepClone(obj[key])
      }
    }
    return newObj
  }
  return obj
}
```

以上代码解决了常见类型的 copy 及循环引用的问题。

当然还是存在一部分缺陷的，比如说递归肯定会存在爆栈的问题，因为执行栈的大小是有限制的，到一定数量栈就会爆掉。当遇到这种问题，我们可以通过遍历的方式来改写递归。也就是如何写层序遍历（BFS）的问题了，只需要通过数组来模拟执行栈就能解决爆栈问题。

但是在面试时能写出上面的代码已经完全够了，剩下的能口述思路基本这道题就能拿到高分了。

### 常见面试题

* 浅拷贝和深拷贝的区别是什么
* `JSON.parse(JSON.stringify(a))` 存在什么问题
* 手写深拷贝函数

## 原型

当我们创建一个对象时 `let obj = { age: 25 }`，我们可以发现能使用很多种函数，但是我们明明没有定义过它们，对于这种情况你是否有过疑惑？

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5be64737f5784013b08180336c976409~tplv-k3u1fbpfcp-zoom-1.image)

当我们在浏览器中打印 `obj` 时你会发现，在 `obj` 上居然还有一个 `__proto__` 属性，那么看来之前的疑问就和这个属性有关系了。

其实每个 JS 对象都有 `__proto__` 属性，这个属性指向了原型。这个属性在现在来说已经不推荐直接去使用它了，这只是浏览器在早期为了让我们访问到内部属性 `[[prototype]]` 来实现的一个东西。

讲到这里好像还是没有弄明白什么是原型，接下来让我们再看看 `__proto__` 里面有什么吧。

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/37fd182d32b44dd6b0dd63a58edbdc91~tplv-k3u1fbpfcp-zoom-1.image)

看到这里你应该明白了，原型也是一个对象，并且这个对象中包含了很多函数，所以我们可以得出一个结论：对于 `obj` 来说，可以通过 `__proto__` 找到一个原型对象，在该对象中定义了很多函数让我们来使用。

在上面的图中我们还可以发现一个 `constructor` 属性，也就是构造函数

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2a3decf3002c4fff97140070a8e9bce8~tplv-k3u1fbpfcp-zoom-1.image)

打开 `constructor` 属性我们又可以发现其中还有一个 `prototype` 属性，并且这个属性对应的值和先前我们在 `__proto__` 中看到的一模一样。所以我们又可以得出一个结论：原型的 `constructor` 属性指向构造函数，构造函数又通过 `prototype` 属性指回原型，但是并不是所有函数都具有这个属性，`Function.prototype.bind()` 就没有这个属性。

其实原型就是那么简单，接下来我们再来看一张图，相信这张图能让你彻底明白原型和原型链

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/208272cb722e403491b0cf3cb4f15797~tplv-k3u1fbpfcp-zoom-1.image)

看完这张图，我再来解释下什么是原型链吧。其实原型链就是多个对象通过 `__proto__` 的方式连接了起来。为什么 `obj` 可以访问到 `valueOf` 函数，就是因为 `obj` 通过原型链找到了 `valueOf` 函数。

对于这一小节的知识点，总结起来就是以下几点：

* `Object` 是所有对象的爸爸，所有对象都可以通过 `__proto__` 找到它
* `Function` 是所有函数的爸爸，所有函数都可以通过 `__proto__` 找到它
* 函数的 `prototype` 是一个对象
* 对象的 `__proto__` 属性指向原型， `__proto__` 将对象和原型连接起来组成了原型链

如果你还想深入学习原型这部分的内容，可以阅读我之前写的[文章](https://github.com/KieSun/Dream/issues/2)。

### 常见面试题

* 如何理解原型及原型链
* 如何重写原型，这个在下一章节的继承小节中有解释
* 原型链指向

## 小结

以上就是全部的常考和容易混乱的基础知识点了，下一章节我们将会学习 ES6 部分的知识。如果大家对于这个章节的内容存在疑问，欢迎在评论区与我互动。
