此文是对常见 JS 基础方面的面试题整理。

继承:原型链

与原型链相关的三个概念:

  • prototype(原型):一个对象,用于实现对象的属性继承
  • constructor(构造函数):一个可以 new 的函数。只有函数才有 prototype。
  • instance(实例):构造函数 new 出来的对象,有一个__proto__属性,等于其父亲的 prototype。
    三者关系见下一篇文章(FLAG!!)

执行上下文

执行上下文,Execution Context,简称 EC

内存空间的分配

首先了解一下执行上下文的存放
JS 的基础数据类型有 Undefined、Null、Boolean、Number、String,这些都是按值访问,存放在栈内存。
其他的 Object 为引用类型,如数组 Array 或其他的自定义对象,这些存放在堆内存,其对应的地址引用(指针)放在栈内存。

EC 的类型

  1. 全局执行上下文
  2. 函数执行上下文
  3. eval 执行上下文

EC 的组成

EC 由一下三部分组成,可以把它当做一个简单的对象

1
2
3
4
5
testEC = {
VO: {},
scopeChain: {},
this: {}
}
  • VO:变量对象,包括当前 EC 中创建的所有变量和函数声明(不包含函数表达式)
  • scopeChain:作用域链,是由当前内存中各个变量对象 VO 串起来的单向链表,每入栈执行一个 function,其对应的 VO 就添加到作用域链的头部,前一个 VO 能自由访问到下一个 VO 上的变量,反过来就不行。

    作用域链可以理解为一组对象列表,包含父级和自身的变量对象,因此我们便能通过作用域链访问到父级里声明的变量或者函数。
    这个对象由两部分构成

    1. [[scope]]属性: 指向父级变量对象和作用域链,也就是包含了父级的[[scope]]和 AO
    2. AO: 自身活动对象
  • this:this 的指向,是在执行上下文被创建时确定的,也就是说 this 是在函数执行时确定的。

执行

先看一段代码

1
2
3
4
5
6
7
8
9
10
11
var color = 'blue'
function changeColor() {
var anotherColor = 'red'
function swapColors() {
var tempColor = anotherColor
anotherColor = color
color = tempColor
}
swapColors()
}
changeColor()

当代码执行时,首先创建全局执行上下文(global EC)并入栈,每进入一个 function,就做一次入栈操作,向栈顶压入一个属于该 function 的新的 EC。若 function 中又调用了另一个 function,则再执行一次入栈…依次执行完再依次出栈,回到全局 EC。全局 EC 一定是在栈底,在浏览器关闭后出栈。

执行上下文图解
执行上下文图解

本节图片来源: 感谢博主

作用域

作用域其实可理解为该上下文中声明的变量的作用范围。可分为 全局作用局、局部作用域(函数作用域)。
ES6 引入 let 后,块级作用域开始起作用。

作用域会声明提前:一个声明在其作用域内都是可见的,函数提升优于变量提升。

函数的父级作用域,是函数定义的时候的父级作用域,不是函数执行的时候的父级作用域,即函数在哪定义,父级作用域就在什么地方

1
2
3
4
5
6
7
8
9
var a = 2
function test() {
console.log(a)
}
function b() {
var a = 3
test()
}
b() //结果为2而不是3

闭包

闭包属于一种特殊的作用域,称为 静态作用域。它的定义可以理解为: 父函数被销毁 的情况下,返回出的子函数的[[scope]]中仍然保留着父级的单变量对象和作用域链,因此可以继续访问到父级的变量对象,这样的函数称为闭包。

经典问题

那个例子:

1
2
3
4
5
for (var i = 0; i < 10; i++) {
clickObj[i].onclick = function() {
console.log(i)
}
}

解析:当 for 循环完成是,声明了 i 个 function,他们的父级作用域都是全局作用域。函数体内的 i 并没有被赋值,而是等待 click 事件,当事件触发时,函数运行,在自己的作用域内没有找到 i ,于是向自己的父级作用域寻找 i,于是找到 i=10

解决:生成闭包,保存父级的作用域。

1
2
3
4
5
6
7
for (var i = 0; i < 10; i++) {
;(function(ind) {
clickObj[ind].onclick = function() {
console.log(ind)
}
})(i)
}

script 引入方式

  1. HTML 静态 <script> 引入
  2. JS 动态插入
  3. <script defer>
  4. <script async>

最稳妥的做法还是把<script>写在<body>底部,没有兼容性问题,没有白屏问题,没有执行顺序问题,高枕无忧,不要搞什么defer和async的花啦~

之间的区别一图胜千言

区别图
区别图

对象的拷贝

  • 浅拷贝:以赋值的形式拷贝引用对象,仍指向同一个地址,修改时原对象也会受到影响
    • Object.assign(target,..source)
    • 展开运算符(…)
  • 深拷贝:完全拷贝一个新对象,与原对象毫不相干。
    • JSON.parse(JSON.stringify(object)):速度最快
      • 如果对象存在循环引用,会报错
      • 当值为 undefined、任意的函数以及 symbol 值,会被忽略
      • 不可枚举的属性会被忽略
    • 递归赋值

new 运算符的执行过程

  1. 生成一个空的简单对象{}
  2. 将该对象的__proto__链接到他的构造函数的 prototype
  3. 将创建的对象作为this的上下文
  4. 如果构造函数没有返回对象,则返回this

继承之圣杯模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let grailMode = (function() {
return function(Origin, Target) {
let Temp = function() {}
Temp.prototype = Origin.prototype
Target.prototype = new Temp()
Target.prototype.constructor = Target
Target.prototype.ancestor = Origin
}
})()

//使用
let Person = function() {}
Person.prototype.sayHello = function() {
console.log('hello')
}

let ChinaPerson = function() {}
grailMode(Person, ChinaPerson)

模块化

现阶段来说,我们在浏览器中使用 ES6 的模块化支持,在 nodejs 中使用 commonjs 的模块化支持。

  1. 分类

    • ES6 import/export
    • commonjs require/module.exports/exports
    • amd require/defined
  2. require 与 import 的区别

    • require支持动态导入,import不支持,正在提案(babel 下支持)
    • require是同步导入,import是异步
    • require是值拷贝,import指向内存地址

函数执行改变 this

  1. call: fn.call(target, 1, 2)
  2. apply: fn.apply(target, [1, 2]) 参数为数组
  3. bind: fn.bind(target)(1,2)

call 和 apply 是立即执行;bind 返回一个函数,等待调用

写在最后

此文持续更新!!!