执行上下文
先来看一段代码:
showname();
console.log(myname);
var myname="Claire";
function showname(){
console.log("I am Claire")
};
这段代码的输出结果为
I am Claire
undefined
那么,在学习JS的时候知道JS是按照顺序执行的,在执行第一句以及第二句的时候,函数showname(),和变量myname都没有被定义,按照之前的说法的话,那么这段代码在执行的过程会被报错,但是能成功运行并且输出相应结果,这里就引入了变量提升的概念
变量提升是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。
什么是声明和赋值???
比如var name="Claire"
就可以拆分成两部分
var name=undefined //声明过程
name="Claire" //赋值过程
再比如
就可以拆分成为:
JS代码的执行流程
从字面上面来看,变量提升就是在物理层面将函数的声明部分和变量的声明部分放到了代码的前面,但不是这样的,而是在编译的时候将它存放到内存里面,,编译完成之后,再对他进行执行
我们用这个过程来解释最初的那段代码,并将它分为两个过程:
变量提升过程:
var myname=undefined;
function showname(){
console.log("I am Claire")
};
执行过程:
showname();
console.log(myname);
myname="Claire";
经过编译后,会生成两部分内容:执行上下文(Execution context)和可执行代码。在执行上下文中存在一个变量环境的对象(Viriable Environment),该对象中保存了变量提升的内容,比如上面代码中的变量myname 和函数 showname,都保存在该对象中。(关于执行上下文下面会讲解)
然后一步一步分析代码
第 1 行和第 2 行,由于这两行代码不是声明操作,所以 JavaScript 引擎不会做任何处 理;
第 3 行,由于这行是经过 var 声明的,因此 JavaScript 引擎将在环境对象中创建一个名 为 myname 的对象,并使用 undefined 对其初始化;
第 4 行,JavaScript 引擎发现了一个通过 function 定义的函数,所以它将函数定义存储 到堆 (HEAP)中,并在环境对象中创建一个 showname 的属性,然后将该属性值指向 堆中函数的位置
1 VariableEnvironment:
2 myname -> undefined,
3 showName ->function : {console.log(myname)
然后到了执行阶段
:
当执行到 showname 函数时,JavaScript 引擎便开始在变量环境对象中查找该函数,由于变量环境对象中存在该函数的引用,所以 JavaScript 引擎便开始执行该函数,并输出函数结果。
接下来打印“myname”信息,JavaScript 引擎继续在变量环境对象中查找该对象,由于变量环境存在 myname 变量,并且其值为
undefined,所以这时候就输出undefined。 接下来执行第 3 行,把“Claire”赋给 myname
变量,赋值后变量环境中的myname 属性值改变为“Claire”
再来看个例子:
foo(); // foo2
var foo = function() {
console.log('foo1');
}
foo(); // foo1,foo重新赋值
function foo() {
console.log('foo2');
}
foo(); // foo1
我们用刚才那个方式来解释一下这一段代码:
首先在编译的过程中会产生这样一个结果: 遇到了第一个 foo 函数,会将该函数体存放到变量环境中。接 下来是第二个 foo 函数,继续存放至变量环境中,但是变量环境中已经存在一个foo 函数了,此时,第二个 foo 函数会将第一个 foo 函数覆 盖掉。这样变量环境中就只存在第二个 showName 函数了
紧接着执行,当执行到 foo 函数时,JavaScript 引擎便开始在变量环境对象中查找该函数,由于变量环境对象中存在该函数的引用,所以 JavaScript引擎便开始执行该函数,并输出函数结果。也就是foo2,然后再执行到变量foo的时候,被同样赋值了一个函数,就相当于两个函数名为foo的函数,此时后面的就会覆盖掉之前的,也就是foo2会被覆盖掉,所以之后的调用都是结果都会打印foo1。
执行上下文、执行上下文栈
在上面将变量提升的时候,说到了执行上下文
执行上下文分为三种:
全局执行上下文
:只有一个,浏览器中的全局对象就是 window 对象,this 指向这个全局对象。当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。
函数执行上下文
:存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文。
Eval 函数执行上下文
:指的是运行在 eval 函数中的代码,很少用而且不建议使用。
执行栈
执行栈,也叫调用栈,具有 LIFO(后进先出)结构,用于存储在代码执行期间创建的所有执行上下文。
首次运行JS代码时,会创建一个全局执行上下文并Push到当前的执行栈中。每当发生函数调用,引擎都会为该函数创建一个新的函数执行上下文并Push到当前执行栈的栈顶。
根据执行栈LIFO规则,当栈顶函数运行完成后,其对应的函数执行上下文将会从执行栈中Pop出,上下文控制权将移到当前执行栈的下一个执行上下文。
var a = 'Hello World!';
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
// Inside first function
// Inside second function
// Again inside first function
// Inside Global Execution Context
执行上下文的创建
执行上下文分两个阶段创建:1)创建阶段; 2)执行阶段
创建阶段
1、确定 this 的值,也被称为 This Binding。
2、LexicalEnvironment(词法环境) 组件被创建。
3、VariableEnvironment(变量环境) 组件被创建。
ExecutionContext = {
ThisBinding = <this value>, // 确定this
LexicalEnvironment = { ... }, // 词法环境
VariableEnvironment = { ... }, // 变量环境
}
词法环境(Lexical Environment)
词法环境有两个组成部分
1、环境记录:存储变量和函数声明的实际位置
2、对外部环境的引用:可以访问其外部词法环境
变量环境
变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属性。 在 ES6 中,
词法 环境
和变量环境
的区别在于前者用于存储函数声明和变量( let 和 const )绑定,而后者仅用于存储变量( var )
绑定。
var,let,const这三个都是用来定义变量的,
区别
1:
var定义的是全局变量,let只能在局部代码块中定义局部变量,而const定义的是一个只读变量
2:var定义的变量未赋值可以提前声明,let定义的变量不能提前声明
3:在let定义的变量所在的区域中,不管这个变量在这个区域外有没有被定义过,都会以目前这个区域中定义的值为主,也就是说,此时会被称为是,暂时性死区
4:let声明过的变量在同一个区域中没有办法再被重新声明,否则就会报错
5:const声明的是一个只读变量,一旦声明,这个变量将不会被更改,但注意定义对象的时候,不是只读,对象的属性还是可以改变。
闭包
闭包是指有权访问另外一个函数作用域中的变量的函数
首先为什么要用闭包,闭包的作用是什么
先来看看菜鸟教程中给的案例,同样一个计数器,不同的写法,不同的效果
var counter = 0;
function add() {
return counter += 1;
}
add(); //1
add(); //2
add(); //3
这种写法中因为counter是一个全局变量,所以在任何地方都可以访问到,每调用一次add(),counter也就自加一次。这样的写法简单,也很方便,但是因为counter它是一个全局变量,任何地方都可以调用,那么它便也很容易被修改,一旦被修改,那么这个计数器的值也会受到影响。
为了避免这样的事情发生,紧接着,菜鸟教程上给出这样一段代码
function add() {
var counter = 0;
return counter += 1;
}
add(); //1
add(); //1
add(); //1
这样的写法将counter变成了局部变量,只能在函数内部调用,不会被外部轻易修改,可是因为每重新调用一次add(),都会讲counter重新初始化为0,这样执行结束后永远都是1
于是出现了闭包
var add = (function () {
var counter = 0;
return function () {return counter += 1;}
})();
add(); //1
add(); //2
add(); //3
闭包指的就是
function () {return counter += 1;}
这个函数。首先解释一下这段代码,在变量add被赋值之前,第一个function执行了一次(执行且仅会执行一次),因为这是一个函数表达式声明方式并且声明后加上了(),所以会自动执行一次。执行后add被赋值(匿名函数)了,add= function () {return counter += 1;}
。然后每次调用add()函数时,返回的都是这个函数,因为这个函数在第一个函数的内部,所以即使第一个函数执行完了,第二个函数依然能访问counter(JS设计的作用域链,当前作用域能访问上级的作用域.
闭包的作用
1、读取函数内部的变量
2、让这些变量的值始终保持在内存中。不会再因为函数调用后被自动清除。
3、方便调用上下文的局部变量。利于代码封装。
其次,再来想一个问题
想之前先说一下作用域的问题
全局作用域 | 函数作用域 |
---|---|
全局作用域中变量的生命周期是永久的,除非你主动销毁; | 而函数作用域中变量的生命周期则是在函数调用结束后就消失 |
那么既然函数中变量的生命周期会在函数执行结束后结束,那么为什么闭包还可以调用到函数中的变量呢?
先来看一个简单的例子:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var foo = checkscope(); // foo指向函数f
foo(); // 调用函数f()
函数f 执行的时候,checkscope 函数上下文已经被销毁了,那函数f是如何获取到scope变量的呢?
因为函数f 执行上下文维护了一个作用域链,会指向checkscope作用域,作用域链是一个数组
fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
所以指向关系是当前作用域 --> checkscope作用域–> 全局作用域,即使 checkscope执行上下文被销毁了,但是 JavaScript 依然会让 这个函数的AO(活动对象) 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,这就是上面说过的与闭包有关的作用。
然后再来说几个例题
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
//3
循环结束后,全局执行上下文的VO是
globalContext = {
VO: {
data: [...],
i: 3
}
}
执行 data[0] 函数的时候,data[0] 函数的作用域链为:
data[0]Context = {
Scope: [AO, globalContext.VO]
}
由于其自身没有i变量,就会向上查找,所有从全局上下文查找到i为3,data[1] 和 data[2] 是一样的。
那么要想避免这样的事情发生,有以下几种方法
一:将其改成闭包,方法就是data[i]返回一个函数,并访问变量i
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function(){
console.log(i);
}
})(i);
}
data[0](); // 0
data[1](); // 1
data[2](); // 2
循环结束后的全局执行上下文没有变化。
执行 data[0] 函数的时候,data[0] 函数的作用域链发生了改变:
data[0]Context = {
Scope: [AO, 匿名函数Context.AO, globalContext.VO]
}
匿名函数执行上下文的AO为:
匿名函数Context = {
AO: {
arguments: {
0: 0,
length: 1
},
i: 0
}
}
因为闭包执行上下文中贮存了变量i,所以根据作用域链会在globalContext.VO中查找到变量i,并输出0。
二:使用es6let
var data = [];
for (let i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
原理:
var data = [];// 创建一个数组data;
// 进入第一次循环
{
let i = 0;
data[0] = function() {
console.log(i);
};
}
循环时,let声明i,所以整个块是块级作用域,那么data[0]这个函数就成了一个闭包。这里用{}表达并不符合语法,只是希望通过它来说明let存在时,这个for循环块是块级作用域,而不是全局作用域。
上面的块级作用域,就像函数作用域一样,函数执行完毕,其中的变量会被销毁,但是因为这个代码块中存在一个闭包,闭包的作用域链中引用着块级作用域,所以在闭包被调用之前,这个块级作用域内部的变量不会被销毁。
当执行data1时,进入下面的执行环境。
{
let i = 1;
data[1] = function(){
console.log(i);
};
}
在上面这个执行环境中,它会首先寻找该执行环境中是否存在i,没有找到,就沿着作用域链继续向上到了其所在的块作用域执行环境,找到了i = 1,于是输出了1。
三:最后还有一种方法就是
使用setTimeout
for (var i = 0; i < 3; i++) {
(function(num) {
setTimeout(function() {
console.log(num);
}, 1000);
})(i);
}
// 0
// 1
// 2
最后的一些注意事项
1:闭包只能取得包含函数中任何变量的最后一个值。因为别忘了闭包所保存的是整个变量对象,而不是某个特殊的变量
2:闭包和内存泄露有关系的地方是,使用闭包的同时比较容易造成循环引用,如果闭包的作用域链中保存着一些DOM节点,这时候就有可能造成内存泄漏
内存泄露举例:
解决:把循环引用中的变量设为null即可,即为切断变量和它此前引用的值之间的连接,当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存
this
说到this,还是先从执行上下文说起,this是和执行上下文绑定的,也就是每个执行上下文中都有this。我们知道执行上下文有:全局执行上下文,函数执行上下文,eval执行上下文
;全局执行上下文的this就是指向window对象,只要说一下函数中的this指向,只要有以下三种方式可以设置上下文的this值
1:通过函数call,apply,bind方法设置
1 let bar = {
2 myName : " 极客邦 ",
3 test1 : 1
4 }
5 function foo(){
6 this.myName = " 极客时间 "
7 }
8 foo.call(bar)
9 console.log(bar)
10 console.log(myName)
同理bind,和apply也可以实现改变this指向的问题
区别:
参考:bind,apply,call区别
2:通过对象调用方法设置
var myObj = {
name : " 极客时间 ",
showThis: function(){
console.log(this)
}
}
myObj.showThis()
我们定义了一个 myObj 对象,该对象是由一个 name 属性和一个
showThis 方法组成的,然后再通过 myObj 对象来调用 showThis 方法。执行这段代码,可以看到,最终输出的 this 值是指向 myObj 的。
再对上面的代码进行改造一下
var myObj = {
name : " 极客时间 ",
showThis: function(){
this.name = " 极客邦 "
console.log(this)
}
}
var foo = myObj.showThis
foo() //打印出来的this指向window
在全局环境中调用一个函数,函数内部的 this 指向的是全局变量 window。
通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身。
3:通过构造函数设置
function CreateObj(){
this.name = " 极客时间 "
}
var myObj = new CreateObj()
console.log(myObj.name) //极客时间
使用this的时候要注意的点:
当函数作为对象的方法调用时,函数中的 this 就是该对象
;当函数被正常调用时,在严格模式下,this 值是 undefined,非严格模式下 this 指向 的是全局对象 window
;嵌套函数中的 this 不会继承外层函数的 this 值
。
最后,我们还提了一下箭头函数
,因为箭头函数没有自己的执行上下文,所以箭头函数的 this 就是它外层函数的 this
。
垃圾回收机制
在JS中常用两种方法来回收垃圾:标记清除和引用计数
标记清除:
在运行的时候,我们给所有储存在内存中的变量都加上标记,然后去掉环境中的变量以及被环境中的变量引用的变量上面的标记,此时,还带有标记的变量就是即将要被删除的变量,因为此时已经无访问到这些变量了,最后,最后垃圾回收处理器就会将这些变量清除掉,销毁那些变量的值释放变量所占用的内存空间。
引用计数
(跟踪记录每个值被引用的次数)当生命一个变量并且用引用类型给这个变量赋值的时候,此时这个值的引用次数为1,当把这个值重新赋值给新的变量的时候,这个值的引用次数会加1,当这个值的引用次数变为0的时候,也就是说没有办法再访问这个值的时候,就会被垃圾处理器回收,,就会释放掉这个值所占用的内存。