JavaScript 基础

作用域和闭包

什么是作用域?

通常来说一段程序代码中使用的变量和函数并不总是可用的,限定其可用性的范围即作用域。

通俗来说,作用域就是函数和变量起作用的区域。

js 存在三种作用域:

  • 全局作用域。声明在函数和代码块之外的变量(在全局的任意地方都可以调用或修改),和在 window 下的属性。
  • 函数作用域(局部作用域)。声明在函数内部的变量,只能在函数内部使用。
  • 块级作用域。ES6 引入的概念。块级作用域通过花括号({})创建,会将 letcount 声明的变量作用域限制在当前代码块中。var 的声明提前会无视块级作用域。

作用域是分层的,子作用域可以沿着链式的作用域链访问父作用域的变量,反之则不行。

那么什么是闭包呢?

当函数(子作用域)存在对父作用域的引用时,为了保证函数的正常执行,这时即使父作用域执行结束关闭了,但引用的变量依然会被保留,不被回收释放,这种特殊的机制就是闭包。

闭包的一个实用作用是创建私有变量和方法。

一个例子是累加器,非闭包的话变量会被全局访问到,被重新定义或修改的话就累加功能就被破坏了,通过闭包就可以规避这个问题。

另一个例子是存储,可以通过闭包实现类似 vuex 的能力。提供特定的方法来增删改查在闭包中的引用值。

闭包的一个经典问题是:在循环中创建闭包:一个常见错误

原因是执行上下文在运行时才确定。

继承与原型链

当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象(object)都有一个私有属性(称之为 __proto__ )指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

为了理解这段话,我们先看看继承是怎么产生的。

1
2
3
4
const name = '张三'
const rename = name.replace('张三', '李四')
console.log(rename)
// output: 李四

上面是这段代码声明了一个 name 常量,并调用 replace()。这时问题就来了,replace() 咋来的呢?

JavaScript 是一种基于原型的语言,这段代码在执行时,name 实际上是 String 的实例:

1
2
3
4
const name = new String('张三') // String {'张三'}
const rename = name.replace('张三', '李四')
console.log(rename)
// output: 李四

作为 String 的实例,name 并不是一个单纯的字符串,而是一个对象。name 的隐式属性 __proto__ 指向 String 的原型。当访问 name 的属性和方法时,会按照原型链依次向上寻找,直至访问到 null 为止。而调用 replace() 就是通过对 String 的继承形成的原型链,访问到了存在于 String 的原型的方法。

String 的原型也有自己的 __proto__,指向 Object 的原型。于是,基于这样的继承关系,完整的原型链如下:

1
name -> String.prototype -> Object.prototype -> null

同理,其他几种数据类型也一样,都是由对应的构造函数创建的实例,所以才说 JavaScript 万物皆对象。

构造函数和原型的关系是:构造函数的 prototype 属性指向原型对象,这个对象包含所有实例共享的方法和属性。原型对象的 constructor 指向构造函数本身。

图源:掘金

构造函数和正常函数没有本质的区别,一个使用 function 正常声明的函数,用 new 操作符调用,就是构造函数。我们也可以自己创建构造函数来实现继承,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person(gender) {
this.gender = gender
}

Person.prototype.body = 1
Person.prototype.eye = 2
Person.prototype.eat = function(food) {
console.log('Ate:', food)
}

const LiHua = new Person('男') // Person {gender: '男'}
LiHua.eat('apple') // Ate: apple

/**
* LiHua 的继承链如下:
* LiHua -> Person.prototype -> Function.prototype -> Object.prototype -> null
*/

箭头函数没有原型,也就没有对应的构造函数,所以无法使用 new 操作符。

new 关键字会进行如下的操作:

  1. 创建一个空的简单 JavaScript 对象,即{}
  2. 为步骤 1 新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象;
  3. 将步骤 1 新创建的对象作为 this 的上下文;
  4. 如果该函数没有返回对象,则返回 this

深拷贝

事情循环机制(event loop)

垃圾回收机制

JavaScript 解释器

作者

朷北

发布于

2022-08-20

更新于

2023-03-18

许可协议

评论