本文为《你不懂的 JavaScript》读书笔记,本文不考虑ES6与严格模式。
作用域链
写程序免不了用到变量,如何查找这些变量,是由一个叫做作用域的东西管理的。作用域通常分为两种:词法作用域和动态作用域。
词法作用域是指变量作用域由书写位置决定。举个例子,foo书写时,a(代码1处)指向的是代码2处声明的变量,当foo调用时(代码4处),foo中的a指向的是代码2而非3处声明的变量,因此,bar()输出2。
function foo() {
console.log(a) // 1
}
function bar() {
var a = 3 // 3
foo() // 4
}
var a = 2 // 2
bar() // 输出 2
动态作用域是指变量作用域由调用位置决定。举个例子,当foo调用时(代码4处),a(代码1处)指向的是代码3而非2处声明的变量,因此,bar()输出3。
function foo() {
console.log(a) // 1
}
function bar() {
var a = 3 // 3
foo() // 4
}
var a = 2 // 2
bar() // 输出 3
js采用词法作用域,作用域负责收集、查找变量。作用域通常以树的方式进行组织,以下面的代码为例进行说明。
var a = 1
var b = 2
function f1() {
var c = 3
var d = 4
function f1_1() {
var e = 5
var f = 6
console.log('**********')
console.log(a)
console.log(b)
console.log(c)
console.log(d)
console.log(e)
console.log(f)
}
function f1_2() {
var g = 7
var h = 8
console.log('**********')
console.log(a)
console.log(a)
console.log(b)
console.log(c)
console.log(d)
console.log(g)
console.log(h)
}
console.log('**********')
console.log(a)
console.log(b)
console.log(c)
console.log(d)
f1_1()
f1_2()
}
function f2() {
var i = 9
var j = 10
function f2_1() {
var k = 11
var l = 12
console.log('**********')
console.log(a)
console.log(b)
console.log(i)
console.log(j)
console.log(k)
console.log(l)
}
function f2_2() {
var m = 13
var n = 14
console.log('**********')
console.log(a)
console.log(b)
console.log(i)
console.log(j)
console.log(m)
console.log(n)
}
console.log('**********')
console.log(a)
console.log(b)
console.log(i)
console.log(j)
f2_1()
f2_2()
}
console.log('**********')
console.log(a)
console.log(b)
f1()
f2()
其作用域示意图如下:
+-----+
| a | global
| b |
+-----+
/ \
/ \
+-----+ +-----+
| c | f1 | i | f2
| d | | j |
+-----+ +-----+
/ \ / \
/ \ / \
+-----+ +-----+ +-----+ +-----+
| e | | g | | k | | m |
| f | | h | | l | | n |
+-----+ +-----+ +-----+ +-----+
f1_1 f1_2 f2_1 f2_2
树的根为全局作用域,每个函数创建一个作用域。
当作用域查找变量时,首先从当前作用域开始查找,如果找不到,就向上一级查找。当抵达最外层的全局作用域时,无论是否找到,查找过程都会停止。
如果在作用域中没有找到变量,js引擎对读变量与写变量的行为是不一致的。当无法找到要读取的变量时,js引擎会抛出ReferenceError,示例如下:
function foo() {
console.log(a)
}
foo()
ReferenceError: a is not defined
at foo
当无法找到要写的变量时,js引擎会在全局作用域中创建并返回该变量,示例如下:
function foo() {
a = 3
}
foo()
console.log(a) // 3
当上级作用域与下级作用域有同名变量时,下级作用域中的变量会屏蔽上级作用域中的变量,示例如下:
function foo() {
var a = 3
console.log(a)
}
var a = 1
foo() // 输出 3
js中除了全局作用域与函数作用域外,是否有块作用域呢?使用下面的代码进行验证:
var a = 1
if (a >0) {
var b = 3;
}
console.log(b) // 输出 3
for (var i = 0; i < 5; i++) {
var j = 3;
}
console.log(i) // 输出 5
console.log(j) // 输出 3
从上面代码的输出结果看,js并没有块作用域。
作用域与对象
声明在全局作用域中的变量是全局对象的同名属性,全局作用域与全局对象本质上是一个东西,就像一个硬币的两面一样。这个全局对象在浏览器中是window。
var a = 3;
console.log(window.a) // 3
window.a = 5
console.log(a) // 5
查找变量是在作用域中查找,而属性则是对象的成员,二者是不同的。
a = 3;
console.log(b) // ReferenceError
console.log(a) // 3
var yehanlin = {
'name': 'Ye Hanlin'
}
console.log(yehanlin.age) // undefined
yehanlin.age = 18;
console.log(yehanlin.age) // 18
隐藏数据
作用域查找变量是单向的,在函数内部声明的变量,函数外部是无法访问的,因此,可以通过函数作用域实现数据隐藏。
var b = 2
function foo() {
var a = 3
console.log(a)
console.log(b)
}
foo() // 3 2
console.log(a) // ReferenceError
上面的代码中,在全局作用域无法访问foo作用域中的变量a。
通常以下面的方式编写代码,并希望用户代码使用setName和getName来访问name属性。
var student = {
name: 'Ye Hanlin',
setName: function (name) {
this.name = name
},
getName: function() {
return this.name
}
}
但是,这种写法无法阻止用户代码直接访问对象属性,达不到隐藏name属性的目的。
student.name = 'Ye Hanlin 2'
console.log(student.name)
使用函数作用域,可达到隐藏name属性的目的。示例如下:
function Student() {
var name
function setName(nam) {
name = nam
}
function getName() {
return name
}
return {
setName: setName,
getName: getName
}
}
var s = Student()
s.setName('Ye Hanlin')
console.log(s.getName()) // Ye Hanlin
console.log(s.name) // undefined
利用函数作用域隐藏数据的特性,可以定义一些立即执行函数用作初始化,这些函数只调用一次,且不会污染全局作用域。
var setting;
var world;
(function init(ratio){
var req = {}
ajax(req, function(data) {
setting.score = data.score
setting.level = data.level
})
world.width = 5 * ratio;
world.height = 3 * ratio;
})(3)
闭包
当函数可以记住并访问所在词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。以上文中的Student为例:
function Student() {
var name
function setName(nam) {
name = nam
}
function getName() {
return name
}
return {
setName: setName,
getName: getName
}
}
var s = Student()
s.setName('Ye Hanlin')
只要setName(setName被s所持有)不被释放,其持有的作用域就不会被释放。类似于资源的引用计数,垃圾收集机制不会回收这些东西。
function timer(timeout, msg) {
setTimeout(function tip () {
console.log(msg)
}, timeout * 1000)
}
timer(60, 'wake up')
console.log('set a timer')
set a timer
// after 60 seconds...
wake up
tip被赋值给浏览器内部的某个变量,timer虽然运行完毕了,但是timer的作用域不会被释放,因为tip持有timer的作用域,而浏览器持有tip。
cb = tip // C++ 代码
超时后,浏览器调用tip,tip运行完毕,浏览器持有tip的变量被释放,timer作用域才会被释放。
cb = null // C++ 代码
其实闭包与对象很类似,都是将数据与算法绑定的一种机制。对象是将算法绑定到数据;闭包则是将数据绑定到算法。
声明提升
很多语言要求先声明变量,然后才可以使用。例如,编译如下的c代码会出错。
int main()
{
a = 1;
int a;
}
a.c:3:2 error: 'a' undeclared (first use in this function)
js与c不同,声明在作用域内的变量,会在整个作用域的范围内有效,例如:
a = 3
var a
console.log(a) // 3
事实上,当你看到var a = 2时,可能会认为这是一个声明,但js引擎将其看成两个语句:var a和a = 2。声明(var a)会在任何代码(a = 2)被执行前被处理。例如,下面的代码:
a = 3
b()
function b() {
}
var a
在声明被提升后,js引擎理解如下:
function b() {
}
var a
a = 3
b()
只有声明本身会被提升,逻辑会留在原地。如果提升改变了代码执行的顺序,会造成非常严重的破坏。例如:
a = 3
var a = 2
console.log(a) // 2
示例中,var a = 2被拆为var a和a = 2,var a被提升,a = 2留在原地,位于a = 3之后。
函数声明会被提升,但是函数表达式却不会被提升。例如:
foo() // TypeError
var foo = function () {
}
声明提升后,js引擎理解如下:
var foo
foo()
foo = function () {
}
这段代码实际上是对undefined执行函数调用操作,因此,会提示类型错误。
重复的var声明会被忽略,例如:
a = 2
var a = 3
var a = 4
var a = 4中的声明会被忽略,声明提升后,js引擎理解如下:
var a
a = 2
a = 3
a = 4
重复的函数声明,后者会覆盖前者,示例如下:
function foo() {
console.log(1)
}
function foo() {
console.log(2)
}
foo() // 2
函数声明和变量声明都会被提升,函数会首先被提升,然后才是变量。
foo() // 2
var foo = function () {
console.log(1)
}
function foo(){
console.log(2)
}
foo() // 1
声明提升后,js引擎理解如下:
function foo() {
console.log(2)
}
foo()
foo = function() {
console.log(1)
}
foo()
因此,第一个foo输出2,第二个foo输出1。
觉得不错,就请您转发