重学JS基础-作用域链和闭包

430 阅读4分钟

一,作用域和作用域链

1.全局作用域

JS有一个全局对象,window,在全局声明的变量都属于window的属性,未使用声明符声明的属性也是window的属性。

var a = 10;
b = 10;
function fun(){
    c = 10;
    var d = 10;
}
fun();
console.log("window.a",window.a);   //10
console.log("window.b",window.b);   //10
console.log("window.c",window.c);   //10
console.log("window.d",window.d);   //undefined

2.函数作用域

我们在定义函数的时候,函数会默认存在一个叫scope的隐式属性,即,保存着函数定义时的信息,这个属性指向一个数组,数组中存的则是一组链式的函数执行上下文。数组的第一项就是函数自身的作用域。

假如我们要访问一个属性,就在这个域中按顺序寻找。所以下面的代码只能打印出b的值,因为a在函数定义的时候并未定义。

var b = 2; 
function foo() { 
	console.log(b); 
	console.log(a); 
} 
(function () { 
	var a = 1; 
	foo(); 
})();

//2
//error

3.作用域链

上面说到函数有一个属性指向一个链式的执行上下文,最下层是函数自己的作用域,而再往上就是父级作用域,最后到达window作用域。

因为在函数被定义的时候,会直接拥有父级模块的作用域,比如在window中被定义的函数,会直接拥有window的作用域。

在函数执行的时候,假如访问某个属性在当前函数中没有,就会在链式的执行上下文中寻找。

看一下例子

var a=10
function fun1(){
	var b=20;
	function fun2(){
	    //...
	}
	fun2();
}
fun1();

如上,通过预编译和作用域链来解读一下代码运行的具体步骤

1.首先,全局存在作用域GO,归window所有
2.定义函数fun1时,会继承window的作用域,其scope属性被创建,指向一个链表,其第一项为GO,
即scope(fun1):GO -->
2执行函数fun1时,会生成一个属于fun1的函数执行上下文AO,这是scope第一项为这个AO对象,
即scope(fun1):AO(fun1) --> GO -->
3.执行函数fun1时,在fun1函数体中,由于定义了函数fun2,所以创建fun2的scope属性,直接继承自fun1,
即scope(fun2): AO(fun1) --> GO -->
4.之后在执行函数fun2时,会创建一个专属于fun2的执行上下文,放入,fun2的scope属性的最顶端,
即scope(fun2): AO(fun2) --> AO(fun1) --> GO -->
5,执行完函数fun2后,销毁其作用域
6,执行完函数fun1后,销毁其作用域

属性访问

假如现在要在函数b中访问一个变量,系统则会到函数b的scope中去寻找,scope是一个数组,它从第0位开始访问,第一位是函数b的作用域,找不到的话会继续想下寻找,即函数a的作用域,

再找不到,便会继续向下,即在window的作用域中寻找,最后也无法找的变量的话,则会抛出错误。

var cc = 123;
function a(){
    function b(){
        var bb = 234;
        aa = 0;
        console.log(cc);
    }
    var aa = 123;
    var cc = 111;
    b();
    console.log(aa);       
}
a();

所以函数执行结果为

111
0

二.闭包

当内部函数被保存到外部时,将会生成闭包。生成闭包后,内部函数依旧可以访问其所在的外部函数的作用域。

1.原理

在内部函数被定义的时候会创建一个属于内部函数的scope属性保存着的作用域链,它会直接继承父函数的作用域链.

当它有对父级函数的变量的访问时,这个作用域链在父级函数销毁时不会被销毁,此时内部函数依旧可以访问父级函数的变量。

2.避免闭包的方法

(1)立即执行函数


(function(){ 
    var a; 
    //code
}());

(function(){
    var a; 
    //code
})();

但是,括号有个缺点,那就是如果上一行代码不写分号,括号会被解释为上一行代码最末的函数调用,产生完全不符合预期,并且难以调试的行为,加号等运算符也有类似的问题。

另一种写法

void function () {
	var a = 100;
	console.log(a);

}();

语义上 void 运算表示忽略后面表达式的值,变成 undefined

(2)使用const和let

3.闭包的使用

使用闭包实现一个计数器

function counterCreate(){
    var count = 0;
    return function(){
        count++;
        console.log(`计数${count}次`);
    }
}
var addCount = counterCreate();
addCount();//计数1次
addCount();//计数2次
addCount();//计数3次
addCount();//计数4次

这里内部函数使用了外部函数的count属性,所以再外部函数销毁之后,count依然可以被内部函数使用,但无法再外部被访问。

4.闭包的优缺点

闭包的好处

  1. 希望一个变量长期存储在内存中
  2. 避免全局变量的污染
  3. 私有成员的存在
  4. 用于缓存

闭包的坏处

容易造成内存泄漏

使用闭包定义对象的私有变量

var Person = (function () {
var privateData = {},
  privateId = 0;
function Person(name) {
  Object.defineProperty(this, "_id", { value: privateId++ });
  privateData[this._id] = {
    name: name,
  };
}
Person.prototype.getName = function () {
  return privateData[this._id].name;
};
return Person;
})();

最后

感谢你能看到这里,如果你觉得这篇文章对你有用的话,不妨点个赞再走呀~