一、LHS和RHS查询
当变量出现在赋值操作的左侧时,进行LSH
查询;当变量出现在赋值操作的右侧,即非左侧时,进行RSH
查询。
或者说
LSH
查询是在找变量容器的本身,从而对其赋值,RHS
是查找某个变量的值
现在我们就要举很多例子来看看
console.log(b)
a = 2
① console.log(...)
需要一个引用才能执行,查是否有console,一次RSH
,再从console对象中查是否有log方法,一次RHS
。要打印出b,那就要查找b的值,这是一个RSH
;为a赋值的时候,则要先找到a这个容器然后再赋值操作,这是一个LSH
。
function foo(a) {
console.log(a);
}
foo(1)
② 对foo
调用,就要去找foo
的值,一次RSH
查询;
实参到形参,a=2的操作,一次LSH
查询;
打印a的时候,查询如①中所述;
function foo(a) {
var b = a;
return a + b;
}
var c = foo(1)
③ 查询foo,一次RHS
为c赋值操作c = ...,一次LHS
传参赋值操作a=...,一次LHS
查询a的值,一次RHS
为b进行赋值b=...,一次LHS
查询a和b的值,两次RHS
二、作用域
作用域是根据名称查找变量的一套规则,从当前的范围逐层往上查找,如下图所示,LSH
和RSH
引用都会在当前作用域往上查。
RHS在任何相关的作用域都取不到变量的值时,则会抛出ReferenceError
异常。如果查到了这个变量,但是你对他进行了不合理的操作,比如:
① 对非函数进行调用
② 引用null
或者undefined
的属性
则会抛出TypeError
异常
LHS在顶层的作用域都找不到变量的话,如果是非严格模式,则会帮我们默认创建一个,默认值为undefined,如果是严格模式,那就不好意思了,一样会抛出ReferenceError
异常。
三、提升
(1)变量的提升
a = 1;
var a;
console.log(a) >>> 1
console.log(b) >>> undefined
var b = 2;
var a
的声明被提前了,所以能成功赋值,也能成功打印出来
var b = 2
可以看成是①var b
; ② b =2
;第一步声明被提前了,第二步则留在原地,等到执行打印时候,b还未被赋值,所以是undefined
。
总结:声明会提升,赋值或者其他运行逻辑会留在原地
(2)函数的提升
foo();
function foo() {
console.log( a );
var a = 2;
}
等价于:
function foo() {
var a;
console.log( a ); >>> undefined
a = 2;
}
foo();
foo
是一个函数的声明,则会将整个函数提升
但是如果是一个表达式
foo(); // TypeError!
bar(); // referenceError
var foo = function bar() {
// ...
};
因为var foo
会被先执行,则foo
未定义,而被当作函数调用,则会出现TypeError
异常,而且此时bar
在赋值给foo
之前,也不能在之前使用。
等价于:
var foo;
foo(); // TypeError
bar(); // ReferenceError
foo = function() {
var bar = ...
// ...
}
举个例子:
foo(); // ->>> 1
var foo;
function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );
};
foo(); // ->>>3
function foo() {
console.log( 1 );
}
var foo = function() {
console.log( 2 );
};
function foo() {
console.log( 3 );
}
第一段代码:对 foo
函数的定义会被提前声明,而后面的var foo
重复定义则会被忽略,所以打印的是1
第二段代码:同样是函数提升,但是后面定义的会覆盖前面的,所以是打印出3
四、作用域闭包
函数与对其状态即词法环境(lexical environment)的引用共同构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在JavaScript,函数在每次创建时生成闭包。
来自MDN文档
那我们就来看看:
① 变量间接传递
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // >>> 2
首先调用了foo
,可以看到这个函数的返回值也是一个函数bar
,bar
函数打印了foo
作用域内的a的值。调用baz
时候,可以正常打印出a的值。这样就间接访问到a了。
② 函数间接传递
var fn;
function foo() {
var a = 2;
function baz() {
console.log( a );
}
fn = baz;
}
function bar() {
fn();
}
foo();
bar(); // 2
foo
被调用时,给fn
全局变量赋值了,是名为baz
的foo
内部函数。调用bar
,间接调用了fn
,由于fn
又指向了baz
函数,间接调用了baz
,而他又能访问到a,即可成功打印。
可能引发的问题:
-
引用变量问题
我们用得最多的,当然是for循环了,那么请看这个经典的例子
function test() {
var result = [];
for (var i = 0; i < 10; i++) {
result[i] = function () {
console.info(i)
}
}
return result
}
test()[0]() // >>> 10
result是一个包含十个函数的数组,按照预要分别打印0-9,但是我们取第一个函数执行,结果却是10。那是因为每个result里面的闭包函数,访问的i都是test函数作用域内的i,循环结束后,a=10,所以每个闭包函数都是访问到a=10时候的值。
解决方法:
function test () {
var result = []
for (var i = 0; i < 10; i++) {
result[i] = (function (j) {
return function () {
console.log(j)
}
}(i))
}
return result
},
this.test()[0]() // >>> 0
我们可以把i的值赋值给内层的j,这样打印的j就是每个函数所形成闭包中自己的j,就不会造成指向同一个变量的问题。
this
指向问题
test () {
var people = {
id: 1,
name: '小飞',
age: 18,
getAge: function () {
return function () {
console.log(this.age)
}
}
}
people.getAge()() // TypeError: Cannot read property 'age' of undefined
},
当使用people
调用getAge
函数时,返回的是打印闭包函数下的age
,而此时是在全局作用域window
下调用的,没有age
这个变量,所以会找不到。
解决方法:
test () {
var people = {
id: 1,
name: '小飞',
age: 18,
getAge: function () {
return (function (that) { // 闭包函数
console.log(that.age)
})(this)
}
}
people.getAge() // 18
},
可以在getAge
函数内将this
指向people
作用域,传递给闭包函数,给予作用域权限访问age
③ 内存泄漏问题
由于闭包会携带包含它的函数的作用域,因为会比其他函数占用更多内容,过度使用闭包,会导致内存占用过多。 内存泄漏是指,一块被分配的内存既不能使用,也不能回收,影响程序性能。
JS
对象和DOM
对象的引用造成的问题:
function test() {
var e = document.getElementByID("container");
e.onclick = function() {
console.log(e.name)
}
}
先创建了DOM
对象,为该对象设置点击事件,引用匿名闭包函数,该函数都可以访问test
作用域内的所有属性,产生了一种关联,但是执行完之后,由于这种关联,DOM
对象无法被回收,形成了内存占用
解决方法:
function test() {
var e = document.getElementByID("container");
var name = e.name
e.onclick = function() {
console.log(name)
}
e = null
}
手动释放DOM对象,保留有用数据
参考文章:
彻底搞懂JS闭包各种坑