var myname = 'cellinlab';
可以将上面代码看成两部分组成:
var myname; // 声明部分
myname = 'cellinlab'; // 赋值部分
对于函数来说:
function foo () {
console.log('foo');
}
var bar = function () {
console.log('bar');
}
函数 foo()
是一个完整的函数声明,没有涉及赋值操作;第二个函数,先声明了变量 bar
,再把 function () {}
赋值给 bar
。可以理解为:
变量提升,是指在 JavaScript 代码执行的过程中,JavaScript 引擎将变量的声明部分和函数的声明部分提升到代码的顶部的“行为”。变量被提升后,会给变量设置一个默认值,默认值为 undefined
。
从字面上看,“变量提升”意味着变量和函数的声明会在物理层面移动到代码的前面。但是,实际上,并不是这样的。实际上变量和函数的声明在代码中的位置是不变的,而是在编译阶段被 JavaScript 引擎放入内存中。
一段 JavaScript 代码在执行前需要被 JavaScript 引擎编译,编译完之后,才会进入执行阶段。
showName();
console.log(myname);
var myname = 'cellinlab';
function showName() {
console.log('showName called');
}
上面代码可以被拆成两部分来看,声明部分:
var myname = undefined;
function showName() {
console.log('showName called');
}
执行部分的代码:
showName();
console.log(myname);
myname = 'cellinlab';
可以看出,输入一段代码,经过编译后,会生成两部分内容:执行上下文 和 可执行代码。
执行上下文是 JavaScript 执行一段代码时的运行环境,如调用一个函数,就会进入这个函数的执行上下文,以确定该函数在执行期间用到的诸如 this
、变量、对象以及函数等。
或者说,在执行上下文中存在一个变量环境的对象(Variable Environment),该对象中保存了变量提升的内容。
可以简单理解是这个样子的:
VariableEnvironment:
myname -> undefined
showName -> function () {
console.log('showName called');
}
变量环境对象生成的过程:
showName();
console.log(myname);
var myname = 'cellinlab';
function showName() {
console.log('showName called');
}
var
声明,因此 JavaScript 引擎将在环境对象中创建一个名为 myname
的属性,并将其初始化为 undefined
;function
定义的函数,所以将函数定义存储到堆(Heap)中,并将函数的引用存储到环境对象中的 showName
属性中;接下来,JavasScript 引擎会把声明以外的代码编译为字节码:
showName();
console.log(myname);
myname = 'cellinlab';
JavasScript 引擎按顺序逐行执行“可执行代码”:
showName
函数时,JavaScript 引擎便开始在变量环境对象中查找该函数,由于变量环境对象中存在该函数的引用,所以 JavaScript 引擎开始执行该函数,输出 showName called
;myname
的值,JavaScript 引擎在变量环境对象中查找该属性,找到 myname
且其值为 undefined
,所以 JavaScript 引擎输出 undefined
;'cellinlab'
赋值给 myname
。VariableEnvironment:
myname -> 'cellinlab'
showName -> function () {
console.log('showName called');
}
function showName() {
console.log('cell');
}
showName();
function showName() {
console.log('cellinlab');
}
showName();
showName
函数,引擎会将该函数存到变量环境对象中。当遇到第二个 showName
函数时,会继续存放,但是发现已经存在一个同名函数,此时,新来的函数会将之前的函数覆盖掉,变量环境对象中的 showName
函数体的内容被更新为新的函数体。cellinlab
。所以,如果一段代码中定义了两个同名函数,那么,最后生效的是晚点定义的函数。
调用栈就是用来管理函数调用关系的一种数据结构。
函数调用就是运行一个函数,具体方法就是使用函数名后加括号:
var a = 2;
function add () {
var b = 10;
return a + b;
}
add();
在执行到函数 add()
之前,JavaScript 引擎会为代码创建全局执行上下文,包含声明的函数和变量。
代码中的全局变量和函数都保存在全局上下文的变量环境中。
执行上下文准备好之后,便开始执行全局代码,当执行到 add
时,JavaScript 引擎识别出这是个函数调用,会进行:
add
函数代码;add
函数代码进行编译,并创建该函数的执行上下文和可执行代码;在执行 JavaScript 时,可能存在多个执行上下文,JavaScript 引擎通过栈来管理执行上下文。
在执行上下文创建好后,JavaScript 引擎会将执行上下文压入栈中,通常将用来管理执行上下文的栈称执行上下文栈,也叫调用栈。
var a = 2;
function add (b, c) {
return b + c;
}
function addAll(b, c) {
var d = 10;
result = add(b, c);
return a + result + d;
}
addAll(3, 6);
上面代码执行流程:
a
、函数 add
和 addAll
都保存到了全局上下文的变量环境对象中addAll
函数,当调用该函数时,JavaScript 引擎会编译该函数,并为其创建一个执行上下文,最后还将该函数的执行上下文压入栈: addAll
函数的执行上下文创建好之后,便进入函数代码的执行阶段add
函数调用语句时,同样会为其创建执行上下文,并将其压入调用栈 add
函数返回时,该函数的执行上下文就会从栈顶弹出,并将 result
的值设置为 add
函数的返回值,也就是 9 addAll
执行最后一个相加操作后并返回,addAll 的执行上下文也会弹出 可以看出,调用栈是 JavaScript 引擎追踪函数执行的一个机制,当一次有多个函数被调用时,通过调用栈就能追踪到哪个函数正在被执行,以及各函数之间的关系。
作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。
在 ES6 之前,作用域只有两种:
块级作用域是一对大括号包裹的一段代码,如函数、判断语句、循环语句,甚至单独的一个 {}
都可以被看做是一个块级作用域。对于支持块作用域的语言,代码块内部定义的变量在代码块外部是访问不到的,并且等该代码块中的代码执行完之后,代码块中定义的变量会被销毁。
因为,在 ES6 之前,是不支持块级作用域的。没了块级作用域,再把作用域内部的变量统一提升无疑是最快速、最简单的设计,但是,这导致了函数中的变量无论是在哪里声明的,在编译阶段都会被提取到执行上下文的变量环境中,所以,这些变量在整个函数体内部的任何地方都是能被访问的。
变量容易在不被察觉的情况下被覆盖掉
var myname = 'cellinlab';
function showName () {
console.log(myname);
if (0) {
var myname = 'cell';
}
console.log(myname);
}
showName();
// undefined
// undefined
// undefined
复制🤏
showName
函数的执行上下文创建后,JavaScript 引擎便开始执行 showName
函数内部的代码cellinlab
;另一个在 showName
函数的执行上下文中,其值是 undefined
myname
值是 undefined
,故 输出 undefined
本应销毁的变量没有被销毁
function foo () {
for (var i = 0; i < 7; i++) {
}
console.log(i);
}
foo(); // 7
i
被提升,当 for
循环结束时,i
并没有被销毁。ES6 引入了 let
和 const
关键字,从此 JavaScript 也有了 块作用域。
function varTest () {
var x = 1;
if (ture) {
var x = 2;
console.log(x); // 2
}
console.log(x); // 2
}
varTest();
使用 let
:
function letTest () {
let x = 1;
if (ture) {
let x = 2;
console.log(x); // 2
}
console.log(x); // 1
}
let
支持块级作用域,在编译阶段,JavaScript 引擎并不会把 if
块中的变量提升,使其在全函数可见范围内。
function foo () {
var a = 1;
let b = 2;
{
let b = 3;
var c = 4;
let d = 5;
console.log(a);
console.log(b);
}
console.log(b);
console.log(c);
console.log(d);
}
foo();
// 1 3 2 undefined undefined
var
声明的变量,在编译阶段全都被存放到变量环境里面let
声明的变量,在编译阶段会被存放到 词法环境 中let
声明的变量并没有被存放到词法环境中let
声明的变量,会被存放在词法环境的一个单独的区域中,该区域中的变量不会影响块作用域外的变量function bar () {
console.log(myName);
}
function foo () {
var myName = 'cell';
bar();
}
var myName = 'cellinlab';
foo();
// cellinlab
在每个执行上下文的变量环境中,都包含一个外部引用,用来指向外部的执行上下文,可以称为 outer。
当一段代码使用了一个变量时,JavaScript 引擎首先会在 “当前的执行上下文” 中查找该变量。如果当前的环境变量中没有找到,会继续在 outer 所指向的执行上下文中查找。
bar
函数 和 foo
函数的 outer 都是指向全局上下文的,也就意味着如果在 bar
函数或者 foo
函数中使用了外部变量,那么 JavaScript 引擎回去全局执行上下文中查找。把这个查找的路径称为 作用域链。
不过,有个疑问,foo
函数中调用的 bar
函数,为什么 bar
函数的外部引用是全局上下文,而不是 foo
函数的执行上下文?这需要了解词法作用域,JavaScript 执行过程中,其作用域链是由词法作用域决定的。
词法作用域指作用域由代码中函数声明的位置来决定的,所以词法作用域就是静态作用域,通过它能够预测代码在执行过程中如何查找标识符。
上图中的词法作用域链是:foo
函数 -> bar
函数 -> main
函数 -> 全局作用域。
词法作用域是代码阶段就决定好的,和函数怎么调用没有关系。
function foo () {
var myName = 'cell';
let test1 = 1;
const test2 = 2;
var innerBar = {
getName: function () {
console.log(test1);
return myName;
},
setName: function (name) {
myName = name;
}
};
return innerBar;
}
var bar = foo();
bar.setName('cellinlab');
console.log(bar.getName());
// 1
// cellinlab
根据词法作用域的规则,内部函数 getName
和 setName
总是可以访问它们的外部函数 foo
中的变量。当 innerBar
对象返回给全局变量 bar
时,虽然 foo
函数已经执行结束,但是 getName
和 setName
函数依然可以使用 foo
函数中的变量。在函数执行完后,其执行上下文弹出了,但是由于返回的方法中使用了 foo
中的变量 myName
和 test1
,所以这两个变量依然存在于内存中,这就是闭包。
在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个外部函数后,即使该外部函数已经执行结束,但是内部函数引用外部函数的变量依然保存在内存中,就把这些变量的集合称为闭包。如外部函数是 foo
,那这些变量的集合就称为 foo
函数的闭包。
通常,如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;如果这个闭包以后不再使用的话,就会造成内存泄漏。
如果引用闭包的函数是局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。
需要注意:如果闭包会一直使用,那么它可以作为全局变量存在;如果使用频率不高,而且占用内存较大的话,就尽量让它成为一个局部变量。
this 是和执行上下文绑定的,即每个执行上下文中都有一个 this
。执行上下文主要有三种:全局执行上下文、函数执行上下文和 eval
执行上下文,那对应的 this
就有三种。
全局执行上下文中的 this
指向的是全局对象,即 window
。这也是 this
和作用域链的唯一交点,作用域链的最低端包含了 window
对象,全局执行上下文中的 this
也是指向 window
对象。
function foo() {
console.log(this);
}
foo();
// window
默认情况下调用一个函数,其执行上下文中的 this
是指向 window
对象的。可以通过一些方法来设置函数执行上下文中的 this
值:
call
方法设置(还有 bind
和 apply
)let bar = {
myName: 'cell',
test1: 1,
};
function foo () {
this.myName = 'cellinlab';
}
foo.call(bar);
console.log(bar.myName); // cellinlab
var myObj = {
name: 'cellinlab',
showName: function () {
console.log(this.name);
}
}
myObj.showName(); // cellinlab
this
指向对象本身var name = 'default';
var myObj = {
name: 'cellinlab',
showName: function () {
console.log(this.name);
this.name = 'cell';
console.log(this.name);
}
};
var foo = myObj.showName;
foo();
// default
// cell
this
指向对象本身function CreateObj () {
this.name = 'cellinlab';
}
var myObj = new CreateObj();
console.log(myObj.name); // cellinlab
new CreateObj()
时,JavaScript 引擎会: tempObj
CreateObj.call
,并将 tempObj
作为 call
的参数,这样当 CreateObj
的执行上下文创建时,它的 this
指向的就是 tempObj
CreateObj
函数,此时的 CreateObj
函数上下文中的 this
指向了 tempObj
对象;tempObj
对象var tempObj = {};
CreateObj.call(tempObj);
var myObj = tempObj;
嵌套函数中的 this
不会从外层函数中继承
var name = 'default';
var myObj = {
name: 'cellinlab',
showName: function () {
console.log(this.name);
function foo () {
console.log(this.name);
};
foo();
}
};
myObj.showName();
// cellinlab
// default
foo
中的 this
指向的是全局 window 对象,而函数 showName
中的 this
指向的是 myObj
对象showName
中声明式地引用 this
来解决这个问题,这种方案的本质是将 this
体系转换为了作用域的体系var name = 'default';
var myObj = {
name: 'cellinlab',
showName: function () {
console.log(this.name);
const self = this;
function foo () {
console.log(self.name);
};
foo();
}
};
myObj.showName();
// cellinlab
// cellinlab
this
取决于它的外部函数var name = 'default';
var myObj = {
name: 'cellinlab',
showName: function () {
console.log(this.name);
const self = this;
const foo = () => {
console.log(self.name);
};
foo();
}
};
myObj.showName();
// cellinlab
// cellinlab
普通函数中的 this
默认指向全局对象 window
如果要让函数执行上下文中的 this
指向某个对象,最好的方法是使用 call
来做显示绑定。
也可以通过 严格模式 来解决,严格模式下,默认执行一个函数,其函数的执行上下文中的 this
是 undefined
。