JavaScript 基础
作用域和闭包
什么是作用域?
通常来说一段程序代码中使用的变量和函数并不总是可用的,限定其可用性的范围即作用域。
通俗来说,作用域就是函数和变量起作用的区域。
js 存在三种作用域:
- 全局作用域。声明在函数和代码块之外的变量(在全局的任意地方都可以调用或修改),和在 window 下的属性。
- 函数作用域(局部作用域)。声明在函数内部的变量,只能在函数内部使用。
- 块级作用域。ES6 引入的概念。块级作用域通过花括号(
{}
)创建,会将let
和count
声明的变量作用域限制在当前代码块中。var
的声明提前会无视块级作用域。
作用域是分层的,子作用域可以沿着链式的作用域链
访问父作用域的变量,反之则不行。
那么什么是闭包呢?
当函数(子作用域)存在对父作用域的引用时,为了保证函数的正常执行,这时即使父作用域执行结束关闭了,但引用的变量依然会被保留,不被回收释放,这种特殊的机制就是闭包。
闭包的一个实用作用是创建私有变量和方法。
一个例子是累加器,非闭包的话变量会被全局访问到,被重新定义或修改的话就累加功能就被破坏了,通过闭包就可以规避这个问题。
另一个例子是存储,可以通过闭包实现类似 vuex
的能力。提供特定的方法来增删改查在闭包中的引用值。
闭包的一个经典问题是:在循环中创建闭包:一个常见错误。
原因是执行上下文在运行时才确定。
继承与原型链
当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象(object)都有一个私有属性(称之为
__proto__
)指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__
),层层向上直到一个对象的原型对象为null
。根据定义,null
没有原型,并作为这个原型链中的最后一个环节。
为了理解这段话,我们先看看继承是怎么产生的。
1 | const name = '张三' |
上面是这段代码声明了一个 name
常量,并调用 replace()
。这时问题就来了,replace()
咋来的呢?
JavaScript 是一种基于原型的语言,这段代码在执行时,name
实际上是 String
的实例:
1 | const name = new String('张三') // String {'张三'} |
作为 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 | function Person(gender) { |
箭头函数没有原型,也就没有对应的构造函数,所以无法使用 new 操作符。
new
关键字会进行如下的操作:
- 创建一个空的简单 JavaScript 对象,即
{}
; - 为步骤 1 新创建的对象添加属性
__proto__
,将该属性链接至构造函数的原型对象; - 将步骤 1 新创建的对象作为
this
的上下文; - 如果该函数没有返回对象,则返回
this
。
深拷贝
事情循环机制(event loop)
垃圾回收机制
JavaScript 解释器
JavaScript 基础