js中的预编译,作用域链,立即执行函数,闭包,包装类

1,503 阅读14分钟

引言

刚刚学完js之后我感觉JavaScript并没有什么东西,好像也不是很难。但是自己运用的时候发现自己学的并不扎实,里面的知识点还是挺多的,甚至有些理解了我还是会出错。所以为了打好基础,我会复习回顾JavaScript的知识点,做好记录,记录是我自己的学习反馈,也是同大家的学习交流,找到自己的不足。这个小记录总结了js中基础且重要的五个问题,看完后能够深入的理解预编译中变量被提升,闭包的作用以及闭包触发哪些问题等知识点。

我会从深度和广度两个方面来叙说。

文章的开始

js语言有两个特点,一个是单线程语言,一个是解释性语言(读一句执行一句)。

知识点1:预编译

1.js执行三步曲

  • 语法分析:通篇分析,通篇扫描一遍看有没有低级错误不进行编译
  • 预编译: 函数整体提升,变量的声明提升
  • 解释执行:解释一行执行一行

ps:定义变量包括变量声明和变量赋值,

任何未经声明就赋值的变量归window所有,window就是全局的域。 一切声明的全局变量非局部变量,就都是window的属性

2.函数中的预编译

预编译发生在函数执行的前一步

1.创建AO对象,AO执行期上下文

2.找出形参和变量声明放到AO{}里面,且值为undefined

3.实参和形参相统一

4.找出函数里面的函数声明并且函数体为值

然后在AO里面拿东西,写的东西是给电脑看的,想拿东西要到电脑里面定义好的地方去拿。而不是视觉上的效果

来看一个栗子

function fn(a) {
    console.log(a);  //function a() {}
    var a = 123;
    console.log(a);  //123
    
    function a() {}
    console.log(a);  //123
    var b = function() {}
    console.log(b);  //function() {}
    
    function d() {}
    console.log(d);  //function d() {}
}

fn(1);

这道题包括形参名,函数名,变量名都一样的情况,所以我们不能根据自己看到的去判断,可是看在编译之前生成的执行期上下文。

2.1第一步:创建AO对象

AO{

}

2.2第二步:找出变量声明和形参,值为undefined

AO{

形参a:undefined
变量声明b:unfined

}

其中有一个形参a,后边的 var a = 123中有又有一个变量a的声明因为前边有一个形参a所以不用重复写。

2.3第三步:形参实参相统一

AO {

a:1;
变量声明b:unfined

}

2.4第四步:找出函数里面的函数声明并且函数体为其值

a,d为函数声明,b为函数表达式

AO {

a:function a() {}

变量声明b:unfined

d:function d() {}

}

这a的值由1变成function a(){},是因为a是一个函数声明,所以a的值由1变成了函数体。此时预编译的过程完成。然后再进行编译,解释一行执行一行。预编译过程中执行的代码在编译的时候就不会再执行,如var a = 123; 在执行的过程中就不会再执行var a,变量声明被提升优先执行了。所以直接执行a = 123.

3.全局中的预编译

全局里的预编译与函数里面的预编译类似,第一步创建GO对象,第二步找出变量声明,没有第三步实参和形参相统一。下面结合具体的例子来看看

3.1暗示全局变量

function test (){
    var a = b = 124;
    console.log(window.a);  //undefined
    console.log(window.b);  //124
}

上面的变量b是没有经过声明就赋值了124,所以归window所

4.预编译的例子

4.1恶心的预编译0

var x =1; var y = z = 0;
function add(n) {
    return n = n+1;
}
y = add(x);
function add(n) {
    return n =n + 3;
}
z = add(x)

请输出x,y,z的值。最后结果应该x为1,y和z都是4。因为在预编译的时候同名的函数会被覆盖,而不是以我们的直观感觉来判断。因此执行所以1,2,4的结果是错误的。

4.1恶心的预编译1

//GO{
//    testfunction test(){};
//    a:undefined;
//    c:234;
// }
function test (){
    console.log(b);      //undefined
    if(a) {
        var b = 100;
    }
    console.log(b);  //undefined
    c = 234;
    console.log(c);// 234
}
var a;

// AO{
//    b:undefined
//  }
test();
a= 10;
console.log(c);//234

在没有执行test()之前 首先生成GO。window === GO

GO{ test:function test(){}; a:undefined; }

执行test生成

AO{ b:undefined } 在test函数中,有一个没有变量声明就赋值的变量c,所以应该归全局所有于是

GO{ test:function test(){}; a:undefined; c:234; }

4.2恶心的预编译2

//GO {
    global: undifined
    fn: function fn(){}
//}
global = 100;
 function fn() {
     console.log(global);
     global = 200;
     conaole.log(global);
     var global = 300;
 }
 
 // AO {
 //     global:undefined
 // }
 fn();
 var global;

首先生成GO,然后再生成AO.第一个打印的global因为fn函数里面就有声明变量global,不需要到GO里面去找。所以第一个打印的就是undefined,不是100,第二个打印global=200.

4.3恶心的预编译3

function bar() {
    return foo;
    foo = 10;
    function foo() {
        
    }
    var foo = 11;
}
console.log(bar());//function foo(){}

首先console.(bar())就是打印出bar的结果,也就是我们要知道返回的foo的值。根据预编译四部曲我们知道函数声明的优先级最高,所以foo:function foo(){}


console.log(bar());
function bar() {
    foo = 10;
    function foo(){
        
    }
    var foo = 11;
    return foo;
}

在最后对foo赋值11,所以打印出来不是函数声明而是11.可能有些掘友会问foo是一个局部变量,为什么可以在全局里面输出呢?那是因为全局里访问的是全局里面的bar函数,foo是函数里面的一个变量。

知识点2:作用域链

作用域链与原型链不同,作用域链[[scope]],这里面scope的第一位存放GO,第二位存放AO。作用域链是执行期上下文的集合。

1.执行期上下文:

函数被执行时会创建一个执行期上下文,一个执行期上下文定义了一个函数执行时的环境,函数每次执行时的执行期上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行期上下文,函数每次执行时,都会把新生成的执行期上下文填充到作用域链的最顶端。函数执行完毕,执行期上下文被销毁。

2.如何查找变量

从作用域链顶端依次向下查找。

function a() {
    function b() {
        function c() {}
        c();
    }
    b();
}
a();

这个例子里面有abc三个函数,函数定义的时候产生GO,函数执行前产生GO.作用域链如下

a defined a.[[scope]] ---> 0:GO
a doing   a.[[scope]] ---> 0: aAO //aAO表示a的AO
                           1: GO
                           
                           
b defined  b.[[scope]] ---> 0: aAO
                            1: GO      
b doing    b.[[scope]] ---> 0: bAO
                            1: aAO
                            2: GO   
                            
                            
c defined  c.[[scope]] --->  0: bAO
                             1: aAO
                             2: GO  
c doing    c.[[scope]] --->  0: cAO
                             1: bAO
                             2: aAO
                             3: GO


知识点3立即执行函数

1.立即执行函数执行完就可以被销毁,节省空间针对初始化功能的函数

//(function () {})()
//(function (){}()) //w3c 建议的方法

上面的括号就是将函数声明变成了函数表达式才可以被执行,至于为什么不写函数名是因为立即函数执行完后函数名被销毁,这两个特点你可能还是不明白,不要着急,看完了立即函数的2,3两点再回来看第一条就明白了。

1.立即执行函数的深入理解

  1. 只有函数表达式才能被立即执行符号执行
 <script>
        function test(a, b, c) {
            var a = 123;
        }(1, 2, 3)

    </script>

最后控制台会报错,而且是低级的语法解析错误。截图如下。因为只有表达式才能够被执行符号()执行。而它只是函数声明所以不能被执行。

如:123为数字表达式,1233+234也叫表达式,不是说只有等于号才叫表达式。

1.1关于=的正确理解

var test = function test() {}应该拆分成var test 和 = function test() {}两部分来理解,前面的是变量声明,后面的由于带有等号所以是函数表达式(变量赋值)。

还有一种函数表达式如果被执行的时候是不会报错的,如:

 <script>
        var test = function test() {
            var a = 123;
            console.log(a);
        }()

    </script>

与此类似,正负号感叹号等可以将函数声明变成函数表达式,乘号和除号不可以。

注意 3.但是出现了一个有趣的现象,test函数被执行完之后再找不到test函数了,相当于立即执行函数。

test值为undefined
4.一个神奇的例子(阿里巴巴考试题)

function test(a, b, c) {
    console.log(a + b + c);
}(1, 2, 3);

不会报错,也不会被执行。按道理是要报错的,但是语法解析将(1, 2,3);看成了函数表达式。

知识点4闭包

内部的函数被保存到外部形成闭包。

闭包的概念 : 一个作用域可以访问另一个函数内部的变量

闭包的作用: 延长变量的作用范围

1.什么是闭包

比如下面的例子里面,a函数里面嵌套了b函数,a函数定义和执行之前生成GO和AO,b函数在定义的时候拿到了a的执行期上下文,并且返回了b函数,那么它的执行期上下文也一同被带回到函数外部。a函数执行完后它的执行期上下文被销毁。外部拿到了被销毁函数a的执行期上下文。

function a() {
var num = 100;
    function b() {
        num ++;
        console.log(num);
    }
    return b;
}

var demo = a();
demo();   //101
demo();   //102

第一次执行demo输出101;b函数被返回,第一次demo执行===b(),在b函数执行前会生成b的AO放在作用域链的第0位,里面没有num,所以王作用域链的第一位也就是a的AO里面找,恰好找到了num=100,num++使aAO{num:101},所以第一次输出num的值是101。然后销毁b的AO

第二次执行demo函数输出102,第二次执行demo也就是执行b函数,b定义的时候拿到的是a的执行期上下文,拿到的是a的劳动成果。b执行之前再次生成b的AO,没有找到mum变量,于是就去a的AO里面寻找,此时aAO{num:101},num ++后,aAO{num:102},然后b执行完后销毁AO。

2.闭包的作用

记得刚开始学习闭包,学完了前男朋友去接我的时候问我学了什么,我说闭包。然后他问我闭包的作用我半天答不上来哈哈哈哈,很多初学者刚刚学完之后都是模棱两可。现在就将闭包的作用进行一次归纳总结。

1.实现共有变量

我们来看一个累加器的例子

function add(){
    var count = 0;
    function demo(){
        count ++;
        console.log(count);
    }
    return demo;
}
var counter = add();
counter();
counter();
counter();
counter();

解析 在这个例子里面,add函数里面的demo函数被返回到外部,也就是add函数执行完被销毁的时候,count变量的值却被带到了外部,然后执行一次counter后count的值就加1,实现了累加器的功能。counter调一次就加一次。

2.可以做缓存

闭包可以当一个隐式存储结构,我们来看一个吃香蕉的例子

function eater() {
    var food = "";
    var obj{
        eat : function() {
            console.log("i am eating" + food);
            food = "";
        }
        push : function() {
            food = myFood;
        }
        return obj;
    }
}

var eater1 = eater();
eater1.push("香蕉");
eater1.eat()// i am eating 香蕉

解析 这里面返回来了一个obj对象,该对象里面有两个函数也一起被返回保存到外部,food这个变量可以push函数使用,也可以eat函数使用。两个函数都可以对它的值进行操作,所以说food相当于一个隐式的存储体。

3.可以实现封装,属性私有化

function Deng(name, wife){
var preparewife = 'xiaozhang';
this.name = name;
this.wife  = wife;
this.divorce = function() {
this.wife = prerarewife;
}
this.saywife = function() {
console.log(preparewife );
}
}
var deng = new Deng()

当控制台访问deng.preparewif的时候,返回结果undefined,preparewife 本来应该是随着函数执行完的执行期上下文销毁而销毁,但是divorce和saywife函数中都有用到preparewife 这个变量,随着this一起被返回,也就是闭包的作用之一—实现封装,实现属性·私有化。所以将隐式,私有,无关,附加的东西放到闭包里面。

4.模块化开发中防止局部污染变量

js作用域

作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。

我们都知道js中作用域分为全局作用域和局部作用域,局部作用域也是函数作用域。 其他的什么块儿啊、神马文件啊统统都认为是一个作用域,有时候因为一些重名问题导致的错误让人莫名其妙,难以调试解决。

命名空间解决变量名冲突

后来我们把变量函数都放到命名空间中去,命名空间就是对象,为了防止变量名不冲突,用一个对象将这些变量都封装起来。

var obj = {
    depart1:{
        chen:{
            name:'ddd'
        },
        zhao: {
            name: 'fff'
        }
    },
    depart2:{
        ling:{
            name:''
        },
        liao: {
            neme:''
        }
    }
}
var linglong = obj.depart1.chen;
chen.name;

上面代码可以将不同的部门的员工给封装起来,比如他们的姓名属性不会冲突。这个解决办法就是命名空间。

闭包解决变量名冲突

回到这里应该如何用闭包防止变量冲突问题。

var name = 'linglong';
var init = (function () {
    var name = 'xiaoqin';
    function callName () {
        console.log(name);
    }
    return fuction (
     callname();
    ) {}
}())
init();

我们来看这个代码,init拿到立即执行函数返回的结果后再执行init函数.就相当于执行callname函数,最后结果是xiaoqin,不是linglong,是因为闭包的原因。

我们来仔细分析,全局中执行callname的时后拿到了立即函数中的name属性作为返回结果,如果立即执行函数中没定义name属性才会获取全局中的name,所以最后的结果是linglong。

总结:我们可以通过在一个函数内定义(这里的立即执行函数)一些复杂的功能,然后定义一个接口(函数)并返回给init,当调用init的时候就会启动这个接口实现功能。并且不污染全局变量。

我们把上面的代码扩展,看看结果是什么

var name = 'linglong';
var init = (function () {
    var name = 'xiaoqin';
    function callName () {
        console.log(name);
    }
    return fuction (
     callname();
    ) {}
}())
init();
var init2 = (function () {
    var name = 'sicheng';
    function callName () {
        console.log(name);
    }
    return fuction (
     callname();
    ) {}
}())
init2();  //sicheng

复习一下,在开发的过程中,不同的人负责不同的功能,但是如果都使用了相同的变量,变量名会产生冲突,为了防止变量名不冲突,讲了两种方法。

虽然现在webpack、gulp等打包工具来解决,但是js是基础,掌握好js学后面才不会吃力。

5.内存泄露

内存泄露就不多说啦,就是闭包内的东西返回到全局中,全局能够拿到闭包内的变量。

构造函数原理

调用new的时候 1.在函数体最前面隐式加上this = {} 2.执行this.xxx = xxx 3.隐式返回this

function Person(name, height) {
    var that = {};
    that.name = name;
    that.height = height;
    return that;
}

var person = Person('eee',189);
var person = Person('xx', 120)

包装类

原始值可以操作属性,隐式三段式.这儿的原始值是string、boolean、number类型。

//包装类
var num = 4;
num.len = 3;
//new Number(4).len = 3;      delete
//
//new Number(4).len 
console.log(num.len);  //undefined
var str ="abcd";
str.length=2;
console.log(str);//abcd

var arr=[1,2,3,4];
arr.length = 2;
console.log(arr);//[1,2]

原始值没有属性和方法的,至于为什么能够调用是经过了一个包装类的过程,自动创建Number的一个对象然后再给予一个len的属性,创建完后又删掉了这个属性,所以最后是undefined。但是数组有length属性,数组length属性可以被修改。

文章的结束

这篇比较长,而且不是同一个时间段写的,不够好的地方请各位看官指出,觉得我的分享有用的话,欢迎大家关注点赞鸭,后面我将会对js中磨人的this指向做一个全面的总结~~~

最后祝祖国生日快乐,永远和平,繁荣昌盛鸭~~~