阅读 778

javascript——闭包

作用域和作用域链

作用域:也叫词法环境,函数和变量可使用的范围。

function fn1(){
	let a = 0;
}
function fn2(){
	let a = 1;
}
复制代码

在上面的例子中有两个函数,这两个函数会分别在堆中开辟空间,存放自己的变量,fn1中的a变量只在fn1的空间内有效,在fn2空间内无法访问到,直接是作用域。


作用域链:每个函数都有一个作用域,在查找变量或者函数的时候会现在本地的作用域查找,如果没有就去上一层的作用域查找,直到找到全局作用域。(类似原型链)

function fn(){
	let a = 0;
    function fn1(){
    	let a = 1;
    	function fn2(){
        	console.log(a);//1
        }
        fn2();
    }
    fn1();
}
fn();
复制代码

执行fn2()的时候要输出a,但是在fn2的范围内没有a变量的声明,就向上一层的fn1()作用域内寻找,找到了a就输出,不用再向上查。

执行栈和执行上下文

执行上下文:就是当前代码所运行的环境,JavaScript 中运行任何的代码都是在执行上下文中运行。

执行上下文可以分为:

全局执行上下文: 这是默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。它做了两件事:1. 创建一个全局对象,在浏览器中这个全局对象就是 window 对象。2. 将 this 指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。

函数执行上下文: 每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。一个程序中可以存在任意数量的函数执行上下文。

Eval 函数执行上下文: 运行在 eval 函数中的代码也获得了自己的执行上下文。

执行上下文的周期:创建->执行->回收

在创建的时候首先做三件事:1.创建变量对象。2.创建作用域链。3.确定this指向。

在执行时对变量进行赋值和代码执行。

回收是执行上下文出栈等待虚拟机回收执行上下文

执行栈

JavaScript 引擎创建了执行上下文栈来管理执行上下文。可以把执行上下文栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。

在图中,先创建一个全局执行上下文入栈,并且开始执行代码,当需要调用bar()的时候,就创建一个bar的函数执行上下文入栈,执行bar内部的代码,待用foo(),就创造一个foo的执行上下文继续入栈。

等foo执行完之后就退出栈,等待被回收,同理Bar退出栈,全局指向上下文会在浏览器关闭时退出栈。

声明提升

变量提升

大部分语言都是先声明变量,然后使用。但是js中不同。

        console.log(a);//undefined
        var a = 1;
        console.log(a);//1
复制代码

在第一行中打印a没有报错,输出了undefined。这是因为js引擎在预处理阶段对变量进行了声明提升,上升到函数体顶部(实际上是在词法环境/作用域中进行了注册)。相当于变成下面这种形式。

        var a;
        console.log(a);//undefined
        a = 1;
        console.log(a);//1
复制代码

函数提升

函数声明也可以进行提升,函数声明提升的时候会在堆里开辟空间,把函数内容以字符串的形式存储进去(不用管逻辑),然后把堆地址返回给函数名。

console.log(f1); // function f1(){}
function f1() {} // 函数声明

console.log(f2); // undefined
var f2 = function() {}; // 函数表达式相当于变量
复制代码

当遇到函数和变量同名且都会被提升的情况,函数声明优先级比较高,因此变量声明会被函数声明所覆盖,但是可以重新赋值。

alert(a); //输出:function a(){ alert('我是函数') }
function a() {
    alert("我是函数");
} //
var a = "我是变量";
alert(a); //输出:'我是变量'
复制代码

测试题

function test(arg) {
    // 1. 形参 arg 是 "hi"
    // 2. 因为函数声明比变量声明优先级高,所以此时 arg 是 function
    console.log(arg)
    var arg = "hello"; // 3.var arg 变量声明被忽略, arg = 'hello'被执行
    function arg() {
        console.log("hello world");
    }
    console.log(arg);
}
test("hi");
/* 输出:
function arg(){
    console.log('hello world') 
    }
hello 
*/
复制代码

var let const

let是ES6规定的,使用方法和var类似,用来声明变量。但是也有很大不同。

1.let 是块作用域,var是函数作用域(如果定义在全局,就是全局作用域)

        {
            var a = 10;
            let b = 11;
        }
        console.log(a);//10
        console.log(b);// ReferenceError
复制代码

2.let 不会变量提升,在声明之前的部分被称为暂时性死区。

if (true) {
  // TDZ开始 暂时性死区
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ结束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}
复制代码

3.let而且会绑定它所在的块作用域,在一个块作用域里不能声明两个变量名相同的变量。 let和var例子

        var map =[];
        for(var i = 0; i < 10; i++){
            map.push(
                {
                    fn:function(){
                        console.log(i);
                    }
                }
            )
        }
        map[2].fn();//10 沿着作用域链向上找,找到了父级的i ,这时候的i是10
复制代码
        var map =[];
        for(let i = 0; i < 10; i++){
            map.push(
                {
                    fn:function(){
                        console.log(i);
                    }
                }
            )
        }
        map[2].fn();//2 let 是块作用域,每次遍历块中都会是一个新的i变量
        
        
        
        //对其进行展开解析的伪代码
         var map =[];
        {
            let i = 0;
            if(i < 10){
                let i = 0;//等于父层的i
                map.push({
                    fn:function(){console.log(i)}
                })
            }
            i++;
            if(i < 10){
                let i = 1;
                map.push({
                    fn:function(){console.log(i)}
                })
            }
            i++;
            if(i < 10){
                let i = 2;
                map.push({
                    fn:function(){console.log(i)}
                })
            }
            
        }

        map[2].fn();//2
复制代码

const

const声明一个只读的常量。一旦声明,它所存的地址就不能改变,对于常量来说,就是这个常量不能改变。对于其他引用类型来说,其值还是可以变得。

同时const和let一样是块级作用域,有暂存性死区,不能再同一个块里声明两个名字相同的变量。

垃圾回收机制

参考:juejin.cn/post/688702…

内存泄漏

内存泄漏是指已经无法再通过 js 代码来引用到某个对象,但垃圾回收器却认为这个对象还在被引用,因此在回收的时候不会释放它。导致了分配的这块内存永远也无法被释放出来。如果这样的情况越来越多,会导致内存不够用而系统崩溃。

浏览器的垃圾回收机制

引用计数法 当该对象不再被其他对象引用的时候就进行回收。

let people = {
	name:'lili';
}   // people 是第一个对这个对象的引用
let jim = people;//jim是第二个引用
let people = null;
let jim = null;//对象被回收
复制代码

这种垃圾回收机制没办法解决相互引用的问题,这里就会造成引用泄露。

标记清除法 当对象不可获得的时候就回收(访问不到这片内存)。

具体过程(相当于森林的广度优先搜索):

1.垃圾收集器找到所有的根,并“标记”它们。

2.然后它遍历并“标记”来自它们的所有引用。

3.然后它遍历标记的对象并标记 它们的 引用。所有被遍历到的对象都会被记住,以免将来再次遍历到同一个对象。

4.如此操作,直到所有可达的(从根部)引用都被访问到。

5.没有被标记的对象都会被删除。

闭包

参考:juejin.cn/post/684490…

闭包就是能够读取其他函数内部变量的函数。

作用:1.可以读取函数内部的变量。2.让这些变量的值始终保持在内存中。

var count=10;//全局作用域 标记为flag1
function add(){
    var count=0;//函数全局作用域 标记为flag2
    function inner (){
        count+=1;//函数的内部作用域
        alert(count);
    }
    return inner
}
var s=add();
s();//输出1
s();//输出2
复制代码

add()调用(count进行声明),然后返回一个inner函数赋给s。

function inner(){
	count+=1;
    alert(count);
}
复制代码

第一次调用s()时,会执行这个inner函数。遇到count这个变量,发现在这个函数的本地作用域没有声明,那么就向上一层作用域找,找到flag2,然后执行代码count变成1(改变的是上一层作用域内的count),输出1。第一次调用结束。

第二次调用s()时,依然是执行这个函数,这时再次寻找count,找到的上一个作用域的count是1(由于垃圾回收机制的作用,所以上一次函数执行结束后没有被回收),执行代码count = 2。

这个例子中s其实就是闭包inner函数,通过它可以在全局环境中访问到函数内部的变量,并且让这些变量保存在内存中,供下一次使用。

测试题

var num = 10;
var obj = {num: 20};

obj.fn = (function a(num){
    this.num = num * 3;
    num++;
    return function b(n) {
        this.num += n;
        num++;
        console.log(num);
    }
})(obj.num)

var fn = obj.fn;
fn(5)
obj.fn(10)
console.log(num,obj.num)
//22 23 65 30
复制代码

首先再全局作用域下对变量和函数进行声明提升。在栈中num、obj、fn都赋值为undefined。

然后执行前两句代码执行之后 window.num = 10,obj.num = 20。

第三行是个立即执行函数,那么就新建一个函数上下文,给函数a传入形参20(obj.num),相当于在函数第一行进行了一个变量声明 var num = 20 即a.num = 20。

函数继续执行,这里的this相当于是普通函数a直接调用,所以this指向window,所以相当于window.num = a.num * 3,即 window.num = 60。

当前a函数的num++,所以a.num = 21。a函数的返回值是一个b函数,这时候就给obj.fn赋值为b函数的地址。

执行fn(5),相当于执行b(5),这时的This还是window,所以 window.num = 60+5=65,然后函数a 的num++,即a.num = 22,打印22

执行obj.fn(10) 执行b(10),this指向obj,obj.num =20+10 =30,然后函数a.num=23,打印23

最后打印 window.num:65 obj.num:30

文章分类
前端
文章标签