JS中的闭包及面向对象编程 — 1、堆栈内存及闭包详解

943 阅读1分钟

1、数据类型核心操作步骤和原理

不要记规律,记原理。

数据类型的核心操作

  • 值类型操作的步骤

  • 对象操作的步骤

  • 函数操作的步骤

  • 堆内存和栈内存

js中的数据类型

1、基本数据类型(值类型)

  • number
  • string
  • boolean
  • null
  • undefined

2、引用数据类型

  • 对象

    {}普通对象

    []数组

    /^$/正则

    Math对象数据类型的

    .....

  • 函数

    function普通函数

    ....

值类型和基本数据类型,它们操作是按值来操作的,引用数据类型是按引用地址来操作的。

js运行在能解析js的内核和引擎中,而我们现在接触到的就是浏览器,所以我们现在的js代码都在浏览器中运行的,浏览器给我们提供了一个供js代码执行的环境,我们把这个环境称之为:全局作用域(window/global)

值类型和引用数据类型的操作原理

1、值类型

直接按值操作,例如:var a = 12;直接把12这个值赋值给变量a(让a变量和12这个值建立了连接关系)

2、对象数据类型

在js中遇到对象,会严格按照如下的步骤操作:

1、浏览器为其开辟一个新的内存空间,为了方便后期可以找到这个空间,浏览器给空间分配一个16进制(0-9,a-f)的地址

2、按照一定顺序,分别把对象的键值对存储到内存空间中

3、把开辟内存的地址赋值给变量(或者其他的东西,可能是当前一个元素的某个事件,比如说odiv.onclick = function();function就是引用的,它也是地址,就相当于把函数的这个地址赋值给odiv.onclick这个事件了。这里我们只是把地址赋值给一个变量或者一个常量,如果是常量,只要让常量存储的地址不变就ok,地址不变,但是空间里面的内容可以改变,因为地址和空间是两个单独的部分体),以后变量就可以通过地址找到内存空间,然后进行一些操作

如何理解:常量存储的地址不变,但是空间里面的内容可以改变。

不管是常量还是变量,存储的只是地址,如果是常量,只要地址不变,不管地址对应的空间怎么变是无所谓的。

这里如果是变量存储的地址,那么这个地址变了是不会报错的

3、函数的操作

创建函数

1、先开辟一个新的内存空间(为其分配了一个16进制的地址)

2、把函数体中编写的js代码当做字符串存储到空间中(函数只创建不执行没有意义,因为存的是字符串)

3、把分配的地址赋值给声明的函数名(function fn 和 var fn 操作原理其实相同,都是在当前作用域中声明了一个名字,此处两个名字是重复的)

执行函数

目的:执行函数体中的代码

1、函数执行的时候,浏览器会形成一个新的私有作用域(只能执行函数体中的代码)供函数体中的代码执行

2、执行代码之前,先把创建函数存储的那些字符串变为真正的js表达式,按照从上到下的顺序在私有作用域中执行

一个函数可以被执行N次,每一次执行相互之间互不干扰(后面会学习两者之间建立的间接关系)

形成的私有作用域把函数体中的私有变量等都包裹起来了(保护起来了),在私有作用域中操作私有变量和外界没关系,外界也无法直接的操作私有变量,我们把函数执行形成的这种保护机制叫做闭包

通过例子解析原理

// js
var a = 12;
var b = a;
b = 13;
console.log(a); 

var o = {name:'张三'};
var p = o;
p.name = '李四';
console.log(o.name);

var m = {name:'小强'};
var n = m;
n = {name:'打不死的小强'}; // 这步会生成一个新的地址,见图解
console.log(m.name);
// 输出
12
李四
小强

图解:

2、堆栈内存及内存释放

栈内存的概念

俗称叫做作用域(全局作用域window/私有作用域),私有作用域有很多,因为函数一执行就会形成一个私有作用域

栈内存的作用

  • 为js代码提供执行的环境(执行js代码的地方)
  • 基本数据类型值是直接存放在栈内存中的

堆内存的作用

  • 存储引用数据类型值的(相当于一个存储的仓库)
  • 对象存储的是键值对
  • 函数存储的是代码字符串

内存释放

在项目中,我们的内存越少性能越好,我们需要把一些没用的内存处理掉

1、堆内存释放

堆内存是用来存引用数据类型的空间,不管是函数也好还是对象也好,都是先开空间存东西,有个地址,最终把地址告诉给一个名字,此时堆内存被这个名字占用,这时候这个堆内存就不能销毁,所以说,比如,我们

var o = {};当前对象对应的堆内存被变量o占用着,堆内存是无法销毁的。那如果想把它销毁怎么销毁呢?

o是占用了这个堆内存,所以不能销毁,那么让o别占用这个堆内存就可以了。所以,我们让o=12,o=12的时候,o之前是一个引用地址,现在这个o不等于之前引用地址了,所以之前的引用地址就不会被占用了,堆内存就销毁了。但是,o=12相当于又开辟了一个12的值,12的值将会在栈内存存到,所以一般我们让

o = null,null是空对象指针(不指向任何的内存空间,不执行任何的堆内存),此时上一次的堆内存就没有被占用了。谷歌浏览器会在空闲时间把没有被占用的堆内存自动释放(销毁/回收)。

这里注意,只有谷歌浏览器才是这种机制。就是你只要让它没被占用了,谷歌浏览器在空闲时间就会自动帮你把它回收销毁了,其他浏览器的机制不一样。

ie浏览器是按记数指针的机制(ie有时候记数会记乱,尤其是引用里面再调引用,引用嵌套的就会记乱,所以会导致内存不会被释放,这种就是内存不能被回收,会产生垃圾,这就叫做内存泄漏)。其他浏览器是存在销毁机制的,只不过销毁机制和谷歌webkit的销毁机制是不一样的,webkit是不需要计数,只需要让他没引用,谷歌浏览器每隔一段时间空闲的时候他都会把这些堆内存从头到尾过一遍,看看哪个被占用,哪个没被占用,没被占用的就会主动被释放销毁。

正常情况下,如果没用的,我们最好手动赋值给null,不管是计数也好还是自动销毁也好,都得让它先不被引用,这就是堆内存的释放。

2、栈内存释放

一般情况下,函数执行形成栈内存,函数执行完,浏览器会把形成的栈内存自动释放;有时候执行完成,栈内存不能被释放(后面写)

全局作用域在加载页面的时候执行,在关掉页面的时候销毁;

总结

作用域就叫栈内存,引用数据类型开的内存空间存东西的就叫堆内存

3、变量提升

  • 基础概念(变量提升和作用域链)
  • 定义变量带var和不带var的区别
  • 只对等号左边的进行变量提升
  • 不管条件是否成立都要进行预解释
  • 重名的处理

概念

在‘当前作用域’中,js代码自上而下执行之前,浏览器首先会把所有带‘var/function’关键字的进行提前的声明或者定义(es6中常量不进行变量提升)

声明(declare):var num;在当前作用域中吼一嗓子我有num这个名了

定义(defined):num=12;把声明的名字赋一个值

带var关键字的只是提前的声明一下;带function关键字的在变量提升阶段把声明和定义都完成了

列子

console.log(num);
console.log(fn);
var num = 13;
//
// fn();
// 放在前面执行也可以,因为带function关键字的
// 在变量提升阶段已经声明加定义了
function fn() {
    console.log(a);
    var a = 10;
    console.log(a);
}
fn();
console.log(num);

图解

结果

定义变量的时候带var和不带var的区别

1、带var

变量提升:在“全局作用域”中,我们声明一个变量,相当于给全局对象window增加了一个属性名

var a; <=> window.a = undefined

// =>变量提升:var a; <=> window.a = undefined;
console.log(a);//—>undefined
var a = 12;//—>window.a=12
console.log(a);// —>12

// 在'全局作用域'中,我们声明一个变量,相当于给全局对象window增加了一个属性名
console.log(window.a);// window['a']

console.log(a)和console.log(window.a)意思是不一样的,console.log(a)相当于把a变量存的值输出,console.log(window.a)相当于把window全局对象里面a这个属性对应的属性值输出了

之所以输出的值一样,是因为在全局作用域当中,我们声明一个变量a相当于给window加了一个a的属性,当a=12的时候,除了让变量a=12,还相当于window.a=12

2、不带var

// console.log(a);//Uncaught ReferenceError: a is not defined
// console.log(window.a);//undefined
a = 12;// <=> window.a=12
console.log(a);// =>12 <=> window.a(就是输出的window.a,只是省略了window)
console.log(window.a);//=>12

在全局作用域中,如果不带var,仅仅是给全局对象设置了一个新的属性名(把window.省略了),这时不应该再叫变量了。

所以在设置这个属性之前就console.log(a);就会报错Uncaught ReferenceError: a is not defined。

因为这时候属性a不存在,也不是变量a,浏览器识无法识别就会直接报错。

但是如果强制加window.相当于直接判断是不是window的一个属性,没有的话结果就是undefined但不会报错。

总结

带var:在当前作用域中声明了一个变量,如果当前是全局作用域,也相当于个全局作用域设置了一个属性叫做a

不带var:在全局作用域中,如果不带var,仅仅是给全局对象设置了一个新的属性名(把window.省略了),这时不应该再叫变量了

最后

以后项目中,如果你的目的是创建变量,最好不要省略var,这样会严谨一些

4、作用域链初步讲解

function fn() {
    a = 12;//<=>window.a=12 这个不是变量a,这相当于给window加了一个属性a,属性值是12
    console.log(a);//=>12
}
fn();
console.log(a);//=>12

作用于链

函数执行形成一个私有的作用域(保护私有变量),进入到私有作用域中,首先变量提升(声明过得变量是私有的),接下来代码执行

1、执行的时候遇到一个变量,如果这个变量是私有的,那么按照私有处理即可

function fn() {
    // => [私有作用域]
    // 变量提升:var a;(私有变量)
    console.log(a);// undefined
    var a = 12;
    console.log(a);// 12
}
fn();
// 闭包机制:函数执行相当于一个私有作用域保护里面的"私有变量"不受外界的干扰
console.log(a);//Uncaught ReferenceError: a is not defined

2、如果当前这个变量不是私有的,我们需要向他的上级作用域进行查找,上级如果也没有,则继续向上查找,一直找到window全局作用域为止,我们把这种查找机制叫做作用于链

1)如果上级作用域有这个变量,我们当前操作的都是上级作用域中的变量(假如我们在当前作用域把值改了,相当于把上级作用域中的这个值给修改了)

2)如果上级作用域中没有这个变量(找到window也没有):

变量=值:相当于给window设置了一个属性,以后再操作window下就有了

alret(变量)/console.log(变量):想要输出这个变量,但是此时是没有的,所以会报错

function fn() {
    // console.log(a);// ReferenceError: a is not defined
    a = 12;//<=>window.a=12 没带var不是私有变量a,上级也找不到变量a,所以这相当于给window加了一个属性a,属性值是12
    console.log(a);//=>12
}
fn();
console.log(a);//=>12

3、一个测试题


console.log(x,y);
var x = 10,
    y = 20;
function fn() {
    console.log(x,y);
    var x = y = 100;
    console.log(x,y);
}
fn();
console.log(x,y);



// var x = 10,
//     y = 20;
// // 等价于:
// var x = 10;
// var y = 20;

// var x = y = 100;
// // 等价于:
// var x = 100;
// y = 100;// -> 此处的y是不带var的

// 输出
undefined undefined
undefined 20
100 100
10 100


// 代码走势
// // => 变量提升:var x; var y; fn = AAAFFF111;
// console.log(x,y);// => undefined*2
// var x = 10,
//     y = 20;// => x = 10    y = 20
// function fn() {
//     // => [私有作用域]
//     // => 变量提升:var x; (x是私有变量)
//     console.log(x,y);// => undefined 20
//     var x = y = 100;// => x=100(私有)  y=100(全局)
//     console.log(x,y);// => 100  100 
// }
// fn();
// console.log(x,y);//=> 10  100

5、只能对等号左边进行变量提升

=:赋值,左边是变量,右边都应该是值

// 之前
i%2===0?item.className='c1':item.className='c2';

// 现在 
// 等号右边三元运算符计算成值赋值给等号左边,
// 所有右边不管是什么,最后都会计算成值
item.className=i%2===0?'c1':'c2';
// 匿名函数:函数表达式(把函数当做一个值赋值给变量或者其他内容)
oDiv.onclick = function(){}
// 等价于:oDiv.onclick = aaafff000

只对等号左边进行变量提升,右边是值,不会提前什么什么的。如下:

// 变量提升:var fn;
console.log(fn);// undefiend
var fn = function(){

}
console.log(fn);// 函数本身

真实项目中,应用这个原理,我们创建函数的时候可以使用函数表达式的方式:

1、 因为只能对等号左边的进行提升,所以变量提升完成后,当前函数只是声明了,没有定义,想要执行函数只能放在赋值的代码之后执行(放在前面执行相当于让undefiend执行,会报错的)

2、这样让我们的代码逻辑更加严谨,以后想要知道一个执行的函数做了什么功能,只需要向上查找定义的部分即可(不会存在定义的代码在执行下面的情况)

// 项目中经常用函数表达式来创建函数
sun();// sun is not a function
var sun = function(){

}
sun();// 执行不报错



fn();// 执行不报错
function fn(){

}
var fn = function sun(){
    console.log(sun);// 函数本身 [Function: sun]
    console.log(1);
    console.log(arguments.callee);//[Function: sun]
};
// sun();//sun is not defined
fn();

6、不管条件是否成立都要进行变量提升

不管条件是否成立,判断体中出现的var/function都会进行变量提升;但是在最新版本浏览器中,function声明的变量只能提前声明不能定义了(前提:函数式在判断体中)

console.log(num); // undefined
console.log(fn); // undefined
if(1===1){
    var num = 12;
    function fn(){

    }
}
console.log(fn);//undefined
for(var i=0;i<1;i++){ //不管条件是否成立,判断体中出现的var/function都会进行变量提升;
    function fn(){

    }
}

代码执行到条件判断地方

条件不成立:进入不到判断体中,赋值的代码执行不了,此时之前声明的变量或者函数依然是undefined

条件成立:进入条件判断体中的第一件事情不是代码执行,而是把之前变量提升没有定义的函数首先定义了(进入到判断体中函数就定义了:迎合ES6中的块级作用域)

if(1===1){
    console.log(num);// undefined
    console.log(fn);// 函数体本身

    var num = 12;
    function fn(){

    }
    
    console.log(num);// 12
    console.log(fn);// 函数体本身
}
console.log(fn);// 函数体本身(不存在块级作用域的概念,只是迎合ES6中的块级作用域。)

老版本浏览器(参考ie8)不是这样处理的,不管条件是否成立,都要进行变量提升(和新版本不一样的地方,新版本function只是声明,老版本function依然是声明+定义)

小列子

新版本浏览器下

// => 变量提升:没有
f = function () {return true;}
g = function () {return false;}
~function () {
    // => [私有作用域]
    // 变量提升:g=undefined(不管条件是否成立都要进行变量提升,但是新版本的浏览器只对函数进行声明)
    // g()=>undefined() 
    if (g() && [] == ![]) { // TypeError: g is not a function
        f = function () {return false;};
        function g() {return true;
        
        }
    }
}();
console.log(f());
console.log(g());

老版本浏览器下(ie8下)

// => 变量提升:没有
f = function () {return true;}
g = function () {return false;}
~function () {
    // => [私有作用域]
    // 变量提升:g(不管条件是否成立都要进行变量提升,但是老版本的浏览器对函数进行声明+定义)
    // g() => true  []是true  ![]是false  [] == ![] => []和false比较 => 一个对象和一
    // 个boolean比较 => 都会转化成数字比较,[]变成0,false变成0,0==0 所以[] == ![]是true
    if (g() && [] == ![]) { // true
        f = function () {return false;}; // 因为声明的时候没有f,所以会向上一级作用域查找,把window下的f改成false
        function g() {return true; // 声明的时候有g,g是私有的,所以不会改变全局下的g
        
        }
    }
}();
console.log(f());//false
console.log(g());//false

总结:

等号左边的进行变量提升,右边不管

带var和不带var的区别

条件成立不成立的区别,在新老版本下有区别

关于这种 [] == ![] 改怎么去判断

7、重名情况下的处理

在变量提升阶段,如果名字重复了,不会重新的进行声明,但是会重新的进行定义(后面赋的值会把前面赋的值给替换掉)

// => 变量提升:fn=aaafff111 (=aaafff222) (=aaafff333) (=aaafff444)
fn();// => 4
function fn() {console.log(1);}
fn();// => 4
function fn() {console.log(2);}
fn();// => 4
var fn = 13; // => fn = 13
fn();// => 13() => TypeError: fn is not a function(报错后以下不再执行)
function fn() {console.log(3);}
fn();
function fn() {console.log(4);}
fn();

8、私有变量都有哪些

【作用域又属于栈内存,存储引用数据类型的叫堆内存】

全局作用域:window

私有作用域:函数执行

块级作用域:使用let创建变量存在块级作用域(一些机制和私有作用域差不多)

【作用域链】

当前作用域代码执行的时候遇到一个变量,我们首先看一下他是否属于私有变量,如果是当前作用域私有变量,那么以后在私有作用域再遇到这个变量都是操作私有的(闭包:私有作用域保护私有变量不受外界干扰);如果不是私有的变量,向其上级作用域查找,也不是上级作用域私有的,继续向上查找,一直到window全局作用域为止,我们把这种向上一级级查找的机制叫做作用域链;全局下有,操作的就是全局变量,全局下没有(设置:给全局对象window增加了属性名&&获取:报错)

查找私有变量

js中的私有变量有且只有两种

  • 在私有作用域变量提升阶段,声明过的变量(或者函数)
  • 形参也是私有变量

小列子1

function fn(num1,num2){
    var total = num1 + num2;
    return total;
}
var result = fn(10,200);

函数执行形成一个新的私有作用域

1、形参赋值

2、变量提升

3、代码自上而下执行

4、当前栈内存(私有作用域)销毁或者不销毁

小列子2

// => 全局变量提升:var x; var y; var z; fn = AAAFFF000;
var x = 10,
    y = 20,
    z = 30;
function fn(x,y){
    // =>[私有作用域]
    // => 形参赋值:x=10 y=20 (x/y都是私有变量)
    // => 变量提升:var x(忽略的,已经存在x这个名字了)
    console.log(x,y,z);//=>z不是私有变量是全局变量 10 20 30
    var x = 100;// => 私有的x=100
    y = 200;// => 私有的y=200
    z = 300;// => 全局的z=300
    console.log(x,y,z);//=>100,200,300
}
fn(x,y,z);//=> fn执行传递的是实参(实参都是值) fn(10,20,30)
console.log(x,y,z);//=>10,20,300

小列子3

function fn(b){
    // =>[私有作用域]
    // =>形参赋值:b=1 (私有变量)
    // =>变量提升:b=aaafff111 (此处赋值操作替换了形参赋值的内容)
    console.log(b);// => 函数
    function b(){
        // => [私有作用域]
        // => 形参赋值和变量提升都没有
        console.log(b);// => 函数
    }
    b();
}
fn(1);

function fn(b) {
    // =>[私有作用域]
    // =>形参赋值:b=1 (私有变量)
    console.log(b);// => 1
    if (true) {
        function b() {
            // => [私有作用域]
            // => 形参赋值和变量提升都没有
            console.log(b);// => 函数
        }
        b();
    }
}
fn(1);

9、如何查找上级作用域

函数执行形成一个私有的作用域(A),A的上级作用域是谁,和他在哪执行的没关系,主要看他是在哪定义的,在哪个作用域下定义的,当前A的上级作用域就是谁;

小列子1

var n = 10;
function sum(){
    console.log(n);
}
// sum();//=>10

~function (){
    var n = 100;
    sum();// => 10 这里的sum是在全局作用域下定义的,但是sum的宿主环境是当前自执行函数形成的私有作用域
}();

小列子2

var n = 10;
var obj = {
    n:20,
    fn:(function(){
        var n = 30;
        return function(){
            console.log(n);
        }
    })()
};
obj.fn();//=> 30

代码走势图

去掉 var n = 30;

var n = 10;
var obj = {
    n:20,
    fn:(function(){
        // => 上级作用域:全局作用域
        return function(){
            // => 上级作用域:自执行函数
            console.log(n);
        }
    })()
};
obj.fn();//=> 10

小列子3


var n = 10;
var obj = {
    n:20,
    fn:(function(n){
        return function(){
            console.log(n);
        }
    })(obj.n)
};
obj.fn();//=> TypeError: Cannot read property 'n' of undefined

代码走势图

所以当代码执行到fn:(function(n){ return function(){ console.log(n); } })(obj.n),给fn赋值=>执行自执行函数=>需要用到obj.fn=>此时的obj是undefined=>undefined没有n这个属性=>所以报错:TypeError: Cannot read property 'n' of undefined

10、闭包作用之保护(解析部分JQ源码)

闭包作用(保护)

  • 形成私有作用域,保护里面的私有变量不受外界的干扰
  • jQuery:常用的js类库,提供了很多项目中常用的方法(兼容所有浏览器)
  • Zepto:小型JQ,专门为移动端开发准备的

JQ代码片段

// 自执行函数形成私有作用域 window既不是关键字也不是保留字,所以可以作为变量名和形参名来用
(function(window){
    var jQuery = function(){
        ...
    };
    ...

    window.jQuery = window.$ = jQuery;
})(window);// 这个window相当于把全局对象作为实参传给私有变量window,不传也能用window,因为window作为全局对象在哪都能用

jQuery();
$();

Zepto的代码片段

// Zepto是通过return返回,外面一个Zepto的变量接收来实现调用的
var Zepto = (function(){
    var Zepto=function(){
        ...
    };
    ...
    return Zepto;
})();
var $ = Zepto;
Zepto();
$();

// 注意:如果同时引用了jQuery和zepto,$冲突了,jq提供了一个拷贝的方法可以把$的使用权转让的

总结:为了防止代码冲突,我们用闭包机制把它保护起来,外面想引用=>可以通过return或者window.xxx把它直接暴露到全局对象上来用。

真实项目中,我们利用这种保护机制,实现团队协作开发(避免了多人同一个命名,导致代码冲突的问题)

小列子

// 同事A
~function(){
    // =>A写的代码
    function fn(){
        ...
    }
    window.fn = fn;
}();

// 同事B
~function(){
    // =>B写的代码
    function fn(){} 
    // =>由于B里也有fn函数,所以B想调取A写的fn,就不能直接fn()调用,
    // 直接调用是调用的自己的fn函数,这是要通过window去调用
    window.fn();

}();

但是以上写法非常麻烦,如果有多个方法需要暴露出去,就会一直window.xxx,就麻烦。 所以我们通过单列模式来实现模块开发和组件化开发。(后面会写这种模式)

闭包的作用:形成私有作用域保护里面私有的变量私有的函数不受外界干扰,所以说在真正、 项目当中的时候多人协作开发,为了防止A和B代码冲突,A就把所有代码用闭包保护起来, B也 把所有代码用闭包保护起来,当A和B的代码合并到一起的时候就不会冲突了,但是不冲突,项目 直接也不能互相调用代码了,所以为了项目之间也能调用,比如B里面调用A里面的方法,这就需要 在A里面通过window.xxx把方法暴露在全局对象下,在B里面执行的时候,最好通过window.xxx执行 (这样调取就是全局下的xxx方法,而不是自己私有的xxx方法)。

面试官问知道闭包吗?

答:知道。

什么是闭包?

答:函数执行形成的私有作用域保护里面私有变量不受外面干扰的机制叫做闭包

项目中什么地方用到闭包了?

答:项目中多人协作开发的时候,把自己的代码放到一个私有作用域当中,为了避免相互之间的冲突(闭包还有其他作用。后面写)

11、闭包作用之保存(i++ 和 ++i)

闭包作用(保存)

函数执行形成一个私用作用域,函数执行完成,形成的这个栈内存一般情况下都会自动释放。(堆内存被占用不能自动释放,只有把堆内存赋值为null不被暂用是就被释放了=>之前有写过。)

但是还有二般情况:函数执行完成,当前私用作用域(栈内存)中的某一部分内容被栈内存以外的其它东西(变量/元素的事件)占用了,当前的栈内存就不能释放掉,也就形成了不销毁的私用作用域(里面私用变量也不会销毁)

i++和++i的区别

  • i++:先拿原有i的值和其他值进行运算,运算完成后再自身累加1
  • ++i:先自身累加1,然后拿累加完成的结果和其他值进行运算
var i=5;
console.log(5+i++);//=>先运算 5+i运算,运算的结果是10,运算完成之后再进行i++累加的,所以下面的i是6 
// 如果写成5+(i++),加括号也是先运算再累加1
console.log(i);//=>6

i=5;
console.log(5+(++i));//先累加1再运算 => 5+6=11
console.log(i);//=>6

总结:i++是先运算再累加,++i是先累加再运算

例子:

var i=5;
10+(++i)+(i++)+5+(i++)+(++i);//=> 43

// 计算过程如下:
// i=6 16
// 16+(6++) 22 i=7
// 22+5 27
// 27+(7++) 34 i=8
// 34+(++i) i=9 43

12、闭包作用之保存(案列练习)

函数执行形成一个私有作用域,如果私有作用域中的部分内容被以外的变量占用了,当前作用域不销毁

[形式]

函数执行返回了一个引用数据类型堆内存的地址(并且堆内存隶属于这个作用域),外面有一个变量接收了这个返回值,此时当前作用域就不能销毁(想要销毁,只需要让外面的变量赋值为null,也就是不占用当前作用域中的内容了)

小列子

function fn(){
    var i = 1;
    return function(n){
        console.log(n + i++);
    }
}
var f = fn();
f(10);//=>11
fn()(10);//=>11
f(20);//=>22
fn()(20);//=>21

流程图:

1、window全局作用域下 => 变量提升 =>

  • 声明加定义fn

    fn是个函数(引用数据类型),所以声明加定义fn给fn赋值的时候 =>

    2、开辟新的堆内存 AAAFFF000,把函数体中代码块当字符串存下来,再把地址AAAFFF000赋值给fn,fn通过这个地址指向AAAFFFOOO堆内存

  • 声明 var f

3、当全局作用域下变量提升完了,代码开始自上而下执行 =>

  • function fn()就不用管了,已经声明加定义过

  • var f = fn() =>

    f = 把fn执行的返回结果赋值给f =>

4、fn()执行

=> 形成一个私有作用域A

  • 形参赋值(没有形参)
  • 变量提升 => var i;

=>代码自上而下执行

  • 私有变量i = 1;
  • renturn 函数,函数是引用数据类型,引用数据类型操作的都不是值 =>

5、开辟新的堆内存AAAFFF111,把函数体中代码块当字符串存下来"console.log(n+i++)" => 再把地址AAAFFF111返回给renturn

所以:

=>fn()执行的结果是:renturn AAAFFF111

=>f = 把fn执行的返回结果赋值给f:f = AAAFFF111

问题来了:

当fn()执行的时候形成一个私有作用域,在这个私有作用域当中,有变量提升,有私有变量i,return了一个函数,相当于把当前这个函数在这个私有作用域当中定义了,这个函数的引用地址是AAAFFF111;

如果把这个私有作用域叫做A,AAAFFF111函数隶属于A,把A里面的函数地址AAAFFF111告诉给全局下的f,以后全局下的f通过地址AAAFFF111就可以找到A里面的这个堆内存AAAFFF111,也就是说:A里面的一个函数被A外面f占用了,所以这个A不能销毁

结论:

只要return的是一个引用数据类型(函数或者对象)被外面占用,就不能销毁,如果return的是一个值,基本数据类型,就不会存在这个问题。

6、执行f(10)

f通过地址AAAFFF111找到堆内存5,f执行相当于堆内存5里面的字符串"console.log(n+i++)"执行

=> 形成一个私有作用域

  • 形参赋值:n = 10(私有的)
  • 变量提升(没有)

=> 代码从上而下执行

console.log(n+i++);
10+i++
=> 首先执行:10+i (i是A中的)
// (i不是私有的,向上一级作用域查找,f在哪定义的它的上级作用域就是谁,f = AAAFFF111,
// AAAFFF111在作用域A里面创建的,所以f的上级作用域是A,这里的i是作用域A里面的i=1)
=> 10+i执行完后 -> 执行i++ -> i隶属于A -> 所以i++让A中的i=2 

输出11,A中的i变成2保存起来不销毁

=> 这个作用域执行完后就销毁(因为没有被外面引用),但是作用域A中的i变成2保存起来不销毁(因为 作用域A 中堆内存地址AAAFFF111 被外界变量f 引用)

7、代码走到执行fn()(10)

先把fn()执行,把fn()返回的结果(也是一个函数)再执行(并且传递参数10)

8、先把fn()执行

=> 形成一个私有作用域B

  • 形参赋值(没有)
  • 变量提升:var i;

=> 代码从上而下执行

  • i=1
  • return 函数,函数是引用数据类型,开辟一个堆内存 =>

9、开辟堆内存AAAFFF222,把函数体中代码块当字符串存下来"console.log(n+i++)" => 再把地址AAAFFF222返回给renturn

所以:

=>fn()执行的结果是:return AAAFFF222

结论:

fn()每一次执行和上一次执行没有任何关系,从新开辟新的私有作用域,新的栈内存,从新形参赋值,从新变量提升,从新代码自上而下执行,遇到return 函数,也是从新开辟堆内存,从新返回地址,一切都是从新,和上一次没有任何直接的关系。

注意:

这里的作用域B里面的地址AAAFFF222没有被外界引用,但是由于我们要把返回的结果再执行,再执行时会用到这个作用域B,当再执行执行完后,这个作用域B就会销毁,所以这个叫做临时不销毁。

区别:

同上(第4)的作用域A,作用域A也返回了一个引用地址AAAFFF111,但是A把这个返回的引用地址AAAFFF111给了f,只要f一直在,这个地址将一直被占用,所以作用域A不能被销毁。如果把f=null,这个堆内存AAAFFF111将不被占用,作用域A也将被销毁。

10、把fn()执行返回的结果 AAAFFF222 再执行(并且传递参数10)

=> 形成一个私有作用域

  • 形参赋值:n = 10
  • 变量提升(没有)

=> 代码从上而下执行

console.log(n+i++);
10+i++
=> 首先执行:10+i (i是B中的)
// (i不是私有的,向上一级作用域查找,f在哪定义的它的上级作用域就是谁,f = AAAFFF222,
// AAAFFF222在作用域B里面创建的,所以f的上级作用域是B,这里的i是作用域B里面的i=1)
=> 10+i执行完后 -> 执行i++ -> i隶属于B -> 所以i++让B中的i=2 

输出11,B中的i变成2

=> AAAFFF222执行完后,会自动销毁,由于堆内存AAFFF222也没有被外界占用,所以作用域B也会跟着销毁,这里B中的i变成2不会被保存

11、代码执行到f(20)

f(20),全局下的 f = AAAFFF111,找到堆内存 AAAFFF111 中存的字符串 "console.log(n+i++)" 并执行

=> 形成一个私有作用域

  • 形参赋值:n = 20
  • 变量提升(没有)

=> 代码从上而下执行

console.log(n+i++);
20+i++
=> 首先执行:10+i (i是A中的)
// (i不是私有的,向上一级作用域查找,f在哪定义的它的上级作用域就是谁,f = AAAFFF111,
// AAAFFF111在作用域A里面创建的,所以f的上级作用域是A,所以这里的i是作用域A里面的,但是
// A里面的i第一次执行完后等于2,所以这里的i=2)
=> 20+i执行完后 -> 执行i++ -> i隶属于A -> 所以i++让A中的i=3 

输出22,A中的i变成3保存起来不销毁

=> 这个作用域执行完后就销毁。(因为没有被外面引用),但是作用域A中的i变成3保存起来不销毁(因为 作用域A 中堆内存地址AAAFFF111 被全局变量f 引用)

同理这里再执行f(30) => 输出33,A中的i变成4保存起来不销毁

12、执行fn()(20)同执行fn()(10)

先把fn()执行

=> 形成一个私有作用域C

  • 形参赋值(没有)
  • 变量提升:var i;

=> 代码从上而下执行

  • i=1
  • return 函数,函数是引用数据类型,开辟一个堆内存AAAFFF333

=> AAAFFF333里面代码执行,传递一个参数20

  • 20+i++(这里的i是新开辟作用域C下的i=1)

=> 输出21 作用域C下i变成2

=> AAAFFF333执行完后自动销毁,作用域C也跟着销毁,作用域C下i变成2不会被保存

这种题,拿出笔和纸从头到尾一步一步分析,学会画图,10秒钟-1分钟出答案

题一:

var i = 1;
function fn(){
    return function(n){
        console.log(n + i++);
    }

}
var f = fn();
f(10);//11
fn()(10);//12
f(20);//23
fn()(20);//24

题二:

function fn(i){
    return function(n){
        console.log(n + (++i));
    }
}
var f = fn(10);
f(20); // => 31
fn(10)(20); // => 31
f(30); // => 42
fn(20)(10); // => 31
f(40); // => 53

13、js中的this指向

什么是this

  • 当前函数执行的主体(谁执行的函数this就是谁)

  • 函数外面的this是window,我们一般都研究函数内的this指向问题

function fn(){
    console.log(this);// -> this:window
}
fn();
function fn(){
    function b(){
        console.log(this);// -> this:window
    }
    b();
}
fn();
function fn(){
    console.log(this);// -> this:window
}
~function (){
    fn();
}();

由上面的列子可以看出:this是谁和他在哪定义的以及在哪执行的没有任何的关系

在js非严格模式下(默认模式就是非严格模式)

1、自执行函数中的this一般都是window

var obj = {
    fn:(function(){
        // this -> window
        return function(){} // 这个方法中的this?这个方法时赋值给fn还没执行,只有fn执行的时候,才会执行,这时候再区分this是谁。
    })()
}

// 为什么是window?
// => 给obj赋值发现obj是一个对象 
// => 开辟一个新的堆内存(有个地址) 
// => 存obj对象中的属性名和属性值
// => 当存fn这个属性名和属性值的时候,发现fn并不是一个值,而是一个自执行函数
// => 此时自执行函数执行,并把它返回的结果(也是一个函数)对应的堆内存地址赋值给fn 
// => 也就是说给fn赋值的时候,还没有给obj赋值(此时obj是undefined),就会涉及
//    到这个函数自执行,所有此时的this是window

2、给元素的某个事件绑定方法,当事件触发执行对应方法的时候,方法中的this一般都是当前操作的元素本身

oBox.onclick=function(){
    // this-> oBox
}

3、还有一种方式可以快速区分this:当方法执行的时候,看看方法名前面是否有‘点’,有点,点前面是谁this就是谁,没有点this一般都是window

var obj = {
    name:'obj',
    fn:function(){
        console.log(this);
    }
};
obj.fn();//-> this:obj
var f = obj.fn;
f();//-> this:window

在js严格模式下(让js更加严谨)

开启严格模式:在当前作用域的第一行加上‘use strict’,开启严格模式,那么当前作用域下再执行的js代码都是按照严格模式处理的

"use strict";
// => 当前js代码都开启了严格模式(包含了函数中的代码)
~function(){
    'use strict';
    //=> 只是把当前私有作用域开启了严格模式(对外面全局没有影响)
}();

在js严格模式下,如果执行主体不明确,this指向的是undefined(非严格模式下执行的是window)

// 非严格模式
function fn(){
    console.log(this);
}
fn();// -> window
window.fn();// -> window
// 严格模式
'use strict'
function fn(){
    console.log(this);
}
fn();// -> undefined
window.fn();// -> window

小列子

function fn(){
    console.log(this);// this->window
}
document.body.onclick = function(){
    // this->document.body
    fn();
};

14、综合练习(闭包和this结合的面试题)


var num = 1,
    obj = {
        num: 2,
        fn:(function(num){
            this.num *= 2;
            num += 2;

            return function(){
                this.num *= 3;
                num++;
                console.log(num);
            }
        })(num)// => 把全局下的num存储的值赋值给形参
    };
var fn = obj.fn;
fn(); // 4
obj.fn(); // 5
console.log(num,obj.num); // 1 6

为了不消耗内存,最后我们可以手动销毁栈内存A

=> fn = null;

=> obj.fn = null;

=> obj = null;

15、使用闭包解决选项卡循环绑定问题

html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>选项卡</title>
    <link rel="stylesheet" href="1.css" type="text/css" />
</head>
<body>
    <div class="tabBox" id="tabBox">
        <ul>
            <li class="select">新闻</li>
            <li>视频</li>
            <li>其他</li>
        </ul>
        <div class="select">123</div>
        <div>456</div>
        <div>789</div>
    </div>
<script src="10.js"></script>
</body>
</html>

css

*{padding: 0;margin: 0;list-style: none;}
.tabBox{width: 500px;margin: 30px auto;}
.tabBox ul li{display: inline-block;padding: 5px;border:1px solid black;background: grey;}
.tabBox ul li.select{border-bottom: none;background: white;padding-bottom: 6px;}
.tabBox ul li:hover{cursor: pointer;}
.tabBox div{border:1px solid black;height: 280px;line-height: 280px; display: none;margin-top: -1px;text-align: center;}
.tabBox div.select{display: block;}

js(四种解决方案:自定义属性,闭包2中方式,es6)

var tabBox = document.getElementById('tabBox'),
    oList = tabBox.getElementsByTagName('li'),
    oDivList = tabBox.getElementsByTagName('div');

function changeTab(index){
    //=>index:存储当前点击li的索引
    for(var i = 0; i < oList.length; i++){
        oList[i].className = oDivList[i].className = null;
    }
    oList[index].className = oDivList[index].className = 'select';
}


// for(var i = 0; i < oList.length; i++){
//     oList[i].onclick = function(){
//         changeTab(i);//这里传i不行,不行的原因:给当前li点击事件绑定方法的时候,
//         // 绑定的方法并没有执行(点击的时候才执行),循环3次,分别给3个li的点击事件绑定了
//         // 方法,循环完成后i=3(全局的);当点击的时候,执行绑定的方法,形成一个私有的作用
//         // 域,用到了变量i,i不是私有的变量,向全局查找,此时全局的i已经是最后循环的3了
//     }
// }


//1、自定义属性 -----------------------------------------------------------------
// // 以上 -> 解决方案(自定义属性来解决)
// for(var i = 0; i < oList.length; i++){
//     oList[i].myIndex = i;
//     oList[i].onclick = function(){
//         changeTab(this.myIndex);
//     }
// }


// //2-1、(闭包)-----------------------------------------------------
// // 以上 -> 解决方案(闭包机制来解决。形成一个不销毁的私有作用域,
// // 把后续需要用到的结果都保存到私有变量中,以后需要用的时候直接拿过来用)
// for(var i = 0; i < oList.length; i++){
//     oList[i].onclick = (function(i){
//         return function(){
//             changeTab(i);
//         }
//     })(i)
// }

// // i=0
// // oList[0].onclick = (function(i){
// //     // => 私有作用域
// //     // 形参赋值:i=0
// //     return function(){
        
// //     }
// // })(0);
// // 执行自执行函数,形成一个私有作用域,返回了一个引用数据类型地址
// // 这个地址被oList[i].onclick接收,所以这个私有作用域不能销毁,循环
// // 3次,形成3个不销毁的私有作用域,每个不销毁的私有作用域当中,都有一个私有
// // 变量i(因为i是形参,形参属于私有变量),每循环一次i存一个值,


// // 但是这样形成的私有作用域很耗性能



//2-2、(闭包)-------------------------------------------------------------
// 以上 -> 解决方案(闭包机制来解决。)闭包有一个原理,就是在自己形成的私有作用
// 域和全局作用域之间加一层,而且不销毁。
// for(var i = 0; i < oList.length; i++){
//     ~function(i){
//         oList[i].onclick = function(){
//             changeTab(i);
//         }
//     }(i);
// }

// 销毁不销毁不是看它具体什么形式,不是看return没有,而是看它有没有被外界占用;
// 第一次循环形成一个私有作用域,i是私有变量,私有作用域当中有个方法,并且把这个
// 方法赋值给了oList[i].onclick,oList是全局变量,虽然没有return,但是也是把私
// 有作用域中的函数地址赋值给了外面全局变量中的oList,也相当于被外界占用了,所以不销毁


//4、es6 -------------------------------------------------------------------------
// 不考录兼容,用es6中块级作用域解决(var改成letfor(let i = 0; i < oList.length; i++){
    oList[i].onclick = function(){
        changeTab(i);
    }
}

16、阶段汇总之闭包总结与实战应用

闭包汇总

函数执行,形成一个私有作用域,保护里面的私有变量不受外界的干扰,这种保护机制叫做闭包

但是现在市面上,99%的IT开发者都认为:函数执行,形成一个不销毁的私有作用域,除了保护私有变量以外,还可以存储一些内容,这样的模式才是闭包

写个闭包

var utils = (function(){
    return {

    }
})()

// 自执行函数执行形成一个私有作用域,返回了一个地址,地址被外面变量utils接收,
// 所以这个自执行函数执行形成的私有作用域不销毁,这个就是闭包

闭包作用:(项目中哪里用到闭包)

1、保护 => 保护私有变量不受外界干扰

  • 团队协作开发,每个开发者把自己的代码存放在一个私有作用域中,防止合并代码时相互之间的冲突,把需要供别人使用的方法,通过return或者window.xxx暴露在全局下即可
  • jq源码中也是利用保护机制来实现的,首先也是一个自执行函数形成一个闭包,里面有jq的方法,jq的类,通过window.jQuery = window.$ = jQuery把私有变量jQuery这个函数暴露在全局,所以全局下可以用到jq。
~function(){
    var jQuery=function(){
        ...
    }
    ...
    window.$=window.jQuery=jQuery;
}();

2、保存 => 形成一个不销毁的私有作用域

  • 选项卡闭包解决办法
  • 单例模式(js高阶编程技巧:惰性思想/柯理化函数思想...)