浏览器中的 js 执行机制
变量提升
JavaScript 代码执行过程中的变量提升机制
- JavaScript 代码执行过程中,需要先做变量提升,而之所以需要实现变量提升,是因为 JavaScript 代码在执行之前需要先编译。
- 在编译阶段,变量和函数会被存放到变量环境中,变量的默认值会被设置为 undefined;
- 在代码执行阶段,JavaScript 引擎会从变量环境中去查找自定义的变量和函数。
- 如果在编译阶段,存在两个相同的函数,那么最终存放在变量环境中的是最后定义的那个,这是因为后定义的会覆盖掉之前定义的。
- JavaScript 的执行机制:先编译,再执行。
head 头部引入的 js 文件,也是先编译的吗?
html 文件在被加载下来的时候就会进行 dom 解析了,解析过程遇到 script 标签的话就会下载对应的脚本,然后编译脚本,编译完之后就执行脚本,执行完脚本之后继续 dom 解析
调用栈
- 每调用一个函数,JavaScript 引擎会为其创建执行上下文,并把该执行上下文压入调用栈,然后 JavaScript 引擎开始执行函数代码。
- 如果在一个函数 A 中调用了另外一个函数 B,那么 JavaScript 引擎会为 B 函数创建执行上下文,并将 B 函数的执行上下文压入栈顶。
- 当前函数执行完毕后,JavaScript 引擎会将该函数的执行上下文弹出栈。
- 当分配的调用栈空间被占满时,会引发“堆栈溢出”问题。
块级作用域
下面这段代码输出的结果是?
{
let myname = '不一样的少年';
{
console.log(myname);
let myname = '不一样的少年';
}
}
最终打印结果分析与拓展
最终打印结果:VM6277:3 Uncaught ReferenceError: Cannot access 'myname' before initialization
分析原因:在块作用域内,使用 let
声明的变量被提升,但只有变量的创建被提升,初始化并没有被提升。因此,在初始化之前使用变量会导致暂时性死区错误。
拓展:
var
的创建和初始化会被提升,但赋值不会被提升。因此,可以在初始化之前访问到var
声明的变量,其值为undefined
。let
的创建被提升,但初始化和赋值不会被提升。在初始化之前使用let
声明的变量会引发暂时性死区错误。function
的创建、初始化和赋值均会被提升。
案例:
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();
编译并创建执行上下文:
var
声明的变量提升至变量环境中,而let
和const
声明的变量则提升至词法环境中。- 在函数作用域块内部,通过
let
声明的变量并未存放在词法环境中,如下图所示的块级作用域中的b
和d
。
继续执行代码:
- 变量环境中的
a
被赋值为1
,词法环境中的b
被赋值为2
。 - 执行到块级作用域中,
b
被赋值为3
,c
被赋值为4
,d
被赋值为5
。
- 变量环境中的
日志打印的词法环境查询:
- 沿着词法环境的栈顶向下查询,在块级作用域内查找变量。如果找到则返回给 JavaScript 引擎,否则继续在变量环境中查找。
- 沿着词法环境的栈顶向下查询,在块级作用域内查找变量。如果找到则返回给 JavaScript 引擎,否则继续在变量环境中查找。
输出结果如下:
a
为1
- 第一个块级作用域中的
b
为3
- 块级作用域执行完毕后的上下文:
- 当块级作用域执行完毕后,上下文将被弹出栈,
foo
函数的执行上下文变为下图所示。
- 当块级作用域执行完毕后,上下文将被弹出栈,
继续输出以下日志:
b
为2
c
为4
d
:d is not defined
作用域
var bar = {
myName: 'time.geekbang.com',
printName: function () {
console.log(myName);
},
};
function foo() {
let myName = '极客时间';
return bar.printName;
}
let myName = '极客邦';
let _printName = foo();
_printName();
bar.printName();
打印结果:
_printName
函数调用的结果是 “极客邦”bar.printName()
调用的结果也是 “极客邦”
解析
let _printName = foo()
foo 函数返回的是 bar 对象中的 printName 方法,所以此时_printName 就是 printName 方法。javascriptlet _printName = function () { console.log(myName); };
执行_printName 函数,此时_printName 函数定义的位置是在全局作用域,所以执行_printName 函数时,打印输出 myName 值,函数作用域内没有,则向外找,向外找就是全局作用域,即"极客邦"。
执行
bar.printName()
,此时执行的是 bar 对象中的 printName 函数,此时打印 myName 变量,在局部函数作用域内依然是找不到的,所以此时会向外找,所以找的还是全局作用域内的 myName 变量,myName 的值是“极客邦”。
如果要输出以下这个 time.geekbang.com 的话,则需要打印 bar.myName
作用域链
function bar() {
console.log(myName);
}
function foo() {
var myName = '极客邦';
bar();
}
var myName = '极客时间';
foo();
当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量,比如上面那段代码在查找 myName 变量时,如果在当前的变量环境中没有查找到,那么 JavaScript 引擎会继续在 outer 所指向的执行上下文中查找。为了直观理解,你可以看下面这张图:

从图中可以看出,bar 函数和 foo 函数的 outer 都是指向全局上下文的,这也就意味着如果在 bar 函数或者 foo 函数中使用了外部变量,那么 JavaScript 引擎会去全局执行上下文中查找。我们把这个查找的链条就称为作用域链。 JavaScript 执行过程中,其作用域链是由词法作用域决定的。
词法作用域
词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。
例如: 整个词法作用域链的顺序是:foo 函数作用域—>bar 函数作用域—>main 函数作用域—> 全局作用域。
- 词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。所以词法作用域只跟函数定义的位置有关,与函数调用无关
闭包
闭包定义: 在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。
注意事项
闭包如果存在太多的话,就会导致内存泄漏,所以在编写代码的时候需要注意。
例子
function foo() {
var myName = '极客时间';
let test1 = 1;
const test2 = 2;
var innerBar = {
getName: function () {
console.log(test1);
return myName;
},
setName: function (newName) {
myName = newName;
},
};
return innerBar;
}
var bar = foo();
bar.setName('极客邦');
bar.getName();
console.log(bar.getName());
首先我们看看当执行到 foo 函数内部的 return innerBar 这行代码时调用栈的情况,你可以参考下图:
当代码执行到 bar.getName()的时候,此时内部的 test2 变量和 test1 变量都被引用了,所以即使 foo 函数执行完毕,这两个变量依然不会被垃圾回收机制收回。 当 foo 函数执行完毕之后调用栈如下图所示:
无论在哪里调用了 setName 和 getName 方法,它们都会背着这个 foo 函数的专属背包。除了 setName 和 getName 方法,其他任何地方都无法访问这个专属背包,所以称为 foo 函数的闭包
js 执行上下文中的 this
- 当函数作为对象的方法调用时,函数中的 this 就是该对象;
- 当函数被正常调用时,在严格模式下,this 值是 undefined,非严格模式下 this 指向的是全局对象 window;
- 嵌套函数中的 this 不会继承外层函数的 this 值。(除了箭头函数,因为箭头函数没有自己的执行上下文,所以箭头函数的 this 就是它外层函数的 this)
- 点击事件 比如给一个 button 对象绑定点击事件 事件句柄中的 this 就是 button 对象
例 1:
let userInfo = {
name: 'jack.ma',
age: 13,
sex: male,
updateInfo: function () {
//模拟xmlhttprequest请求延时
setTimeout(function () {
this.name = 'pony.ma';
this.age = 39;
this.sex = female;
}, 100);
},
};
userInfo.updateInfo();
使 this 是 userInfo 对象的方式有哪些?
- 改为箭头函数,箭头函数没有自己的执行上下文,所以会取外层的 this,外层的 this 指向的是 userInfo 对象
setTimeout(() => {
this.name = 'pony.ma';
this.age = 39;
this.sex = female;
}, 100);
2.用 self 缓存外层的 this
updateInfo:function(){
//模拟xmlhttprequest请求延时
var self = this
setTimeout(function(){
self.name = "pony.ma"
self.age = 39
self.sex = female
},100)
}
- bind 方法改变 this
updateInfo:function(){
//模拟xmlhttprequest请求延时
const fn = function(){
this.name = "pony.ma"
this.age = 39
this.sex = female
}
setTimeout(fn.bind(this),100)
}
- call 或 apply 方法改变 this
let userInfo = {
name: 'jack.ma',
age: 13,
sex: male,
updateInfo: function () {
//模拟xmlhttprequest请求延时
setTimeout(function () {
(function () {
this.name = 'pony.ma';
this.age = 39;
this.sex = female;
}).call(userInfo); // .apply()
}, 100);
},
};
userInfo.updateInfo();
例 2
function foo() {
console.log(this); // obj1对象
}
var obj1 = {
name: 'obj1',
foo: foo,
};
var obj2 = {
name: 'obj2',
obj1: obj1,
};
obj2.obj1.foo();
- 我们通过 obj2 又引用了 obj1 对象,再通过 obj1 对象调用 foo 函数;
- 那么 foo 调用的位置上其实还是 obj1 被绑定了 this;
例 3
function foo() {
console.log(this);
}
var obj1 = {
name: 'obj1',
foo: foo,
};
// 讲obj1的foo赋值给bar
var bar = obj1.foo;
bar();
此时的 this 是 window 隐式丢失,如果是 obj1.foo()直接调用的话,this 就是 obj1
但是此时是 obj1.foo 赋值给 bar 变量 bar 变量再调用,this 是运行时绑定,所以此时调用的位置是全局直接调用,没有绑定任何对象,所以 this 是默认绑定,即指向的是 window
this 总结
new 绑定 > 显式绑定(bind)> 隐式绑定 > 默认绑定
new 不能和 call 和 apply 同时用
隐式绑定 obj.fn fn 函数中的 this 变成了 obj 对象 此 this 绑定过程为隐式绑定
显式绑定 fn.call(对象) fn.apply(对象) fn.bind(对象)
默认绑定 window
this 是运行时的行为,this 指向什么,完全取决于函数调用时的上下文环境
- 1.函数在调用时,JavaScript 会默认给 this 绑定一个值;
- 2.this 的绑定和定义的位置(编写的位置)没有关系;
- 3.this 的绑定和调用方式以及调用的位置有关系;
- 4.this 是在运行时被绑定的;
new 绑定优先级大于隐式绑定
function foo() {
console.log(this);
}
var obj = {
name: 'why',
foo: foo,
};
new obj.foo(); // foo对象, 说明new绑定优先级更高
显式绑定优先级高于隐式绑定
function foo() {
console.log(this);
}
var obj1 = {
name: 'obj1',
foo: foo,
};
var obj2 = {
name: 'obj2',
foo: foo,
};
// 隐式绑定
obj1.foo(); // obj1
obj2.foo(); // obj2
// 隐式绑定和显式绑定同时存在
obj1.foo.call(obj2); // obj2, 说明显式绑定优先级更高
this 规则之外
忽略显式绑定
如果在显式绑定中,我们传入一个 null 或者 undefined,那么这个显式绑定会被忽略,使用默认规则:
function foo() {
console.log(this);
}
var obj = {
name: 'why',
};
foo.call(obj); // obj对象
foo.call(null); // window
foo.call(undefined); // window
var bar = foo.bind(null);
bar(); // window
间接函数引用
例 1:
var num1 = 100;
var num2 = 0;
var result = (num2 = num1);
console.log(result); // 100
- (num2 = num1)的结果是 num1 的值;
例 2:
function foo() {
console.log(this);
}
var obj1 = {
name: 'obj1',
foo: foo,
};
var obj2 = {
name: 'obj2',
};
obj1.foo(); // obj1对象
(obj2.foo = obj1.foo)(); // window
- 赋值(obj2.foo = obj1.foo)的结果是 foo 函数;
- obj2.foo = obj1.foo:这行代码将 obj1 对象上的 foo 方法赋值给了 obj2.foo。此时,obj2.foo 指向了与 obj1.foo 相同的函数引用。
- (obj2.foo)():紧接着,使用圆括号调用了 obj2.foo。由于这个调用不是通过 obj2 对象来调用的(即不是使用 . 操作符),而是直接调用了 obj2.foo 这个函数引用,所以 this 的值在这个调用上下文中不会指向 obj2,指向的是 window。
ES6 箭头函数中的 this
var obj = {
data: [],
getData: () => {
setTimeout(() => {
console.log(this); // window
}, 1000);
},
};
obj.getData();
this 指向的是 window,因为定时器中用的是箭头函数,没有自己的执行上下文,所以向外找,外层的 getData 函数也是箭头函数,也没有自己的执行上下文,所以继续往外找,就是全局作用域,所以 this 指向的是 window,如果 getData 方法是普通方法的话,this 指向的是 obj
一些例子:
- 例 1:
var name = 'window';
var person = {
name: 'person',
sayName: function () {
console.log(this.name);
},
};
function sayName() {
var sss = person.sayName;
sss(); //
person.sayName();
person.sayName();
(b = person.sayName)();
}
sayName();
解析:
function sayName() {
var sss = person.sayName;
// 独立函数调用,没有和任何对象关联
sss(); // window
// 关联
person.sayName(); // person
person.sayName(); // person
(b = person.sayName)(); // window
}
在 (person.sayName)()
; 这个表达式中,(person.sayName)
将 person.sayName 函数对象包裹在括号中,然后立即执行 ()。 在 JavaScript 中,当一个函数被作为对象的方法调用时,函数内部的 this 指向调用该方法的对象。因此,当执行 (person.sayName)()
时,person.sayName
被作为 person 对象的方法调用,this 指向的是 person 对象。 另一方面,当一个函数被直接调用时,函数内部的 this 指向全局对象(在浏览器中是 window 对象)。在 sss()
和 (b = person.sayName)()
中,函数 sss 和 b 被直接调用,因此 this 指向的是全局对象 window。 所以,在 sayName()
函数中,(person.sayName)()
的 this 指向 person 对象,而其他两个调用的 this 指向全局对象 window。
例 2:
var name = 'window';
var person1 = {
name: 'person1',
foo1: function () {
console.log(this.name);
},
foo2: () => console.log(this.name),
foo3: function () {
return function () {
console.log(this.name);
};
},
foo4: function () {
return () => {
console.log(this.name);
};
},
};
var person2 = { name: 'person2' };
person1.foo1();
person1.foo1.call(person2);
person1.foo2();
person1.foo2.call(person2);
person1.foo3()();
person1.foo3.call(person2)();
person1.foo3().call(person2);
person1.foo4()();
person1.foo4.call(person2)();
person1.foo4().call(person2);
解析:
// 隐式绑定,肯定是person1
person1.foo1(); // person1
// 隐式绑定和显式绑定的结合,显式绑定生效,所以是person2
person1.foo1.call(person2); // person2
/**
foo2是一个箭头函数,不适用于所有的规则,则根据作用域链往外找,
即window
*/
person1.foo2(); // window
// foo2是箭头函数,不适用所有的显式绑定规则,所以call无效,向作用域外找
person1.foo2.call(person2); // window
// 获取foo3,但是调用位置是全局作用域下的,所以默认绑定的是window
person1.foo3()(); // window
// foo3显式绑定到person2中,但是拿到返回函数依然是在全局下调用,所以依然是window
person1.foo3.call(person2)(); // window
// 拿到foo3返回的函数,通过显式绑定到person2中,所以this是person2
person1.foo3().call(person2); // person2
/** foo4()返回的是一个箭头函数,没有自己的执行上下文,所以箭头函数的执行找上层
作用域,是person1*/
person1.foo4()(); // person1
/* person1.foo4函数中的this原本指向的是person1 现在显式绑定之后person1.foo4
函数内的this绑定为person2 又因为person1.foo4.call(person2)返回的是
箭头函数,箭头函数没有执行上下文,所以不是在全局下调用,而是找函数执行的上层
作用域,即person1.foo4函数,此时person1.foo4的this为person2,所以箭头函数
输出的this为person2 */
person1.foo4.call(person2)(); // person2
// foo4返回的是箭头函数,箭头函数不能显式绑定,所以只看上层作用域,this指向person1
person1.foo4().call(person2); // person1
例 3:
var name = 'window';
function Person(name) {
this.name = name;
(this.foo1 = function () {
console.log(this.name);
}),
(this.foo2 = () => console.log(this.name)),
(this.foo3 = function () {
return function () {
console.log(this.name);
};
}),
(this.foo4 = function () {
return () => {
console.log(this.name);
};
});
}
var person1 = new Person('person1');
var person2 = new Person('person2');
person1.foo1();
person1.foo1.call(person2);
person1.foo2();
person1.foo2.call(person2);
person1.foo3()();
person1.foo3.call(person2)();
person1.foo3().call(person2);
person1.foo4()();
person1.foo4.call(person2)();
person1.foo4().call(person2);
解析:
// 隐式绑定
person1.foo1(); // person1
// 显式绑定优先级大于隐式绑定
person1.foo1.call(person2); // person2
// foo是一个箭头函数,会找上层作用域中的this,那么就是person1
person1.foo2(); // person1
// foo是一个箭头函数,使用call调用不会影响this的绑定,根据作用域向上层查找
person1.foo2.call(person2); // person1
// person1.foo3()返回的是普通函数,此处是在全局直接执行,所以this是默认绑定window
person1.foo3()(); // window
// person1.foo3()返回的是普通函数,此处是在全局直接执行,所以this是默认绑定window
person1.foo3.call(person2)(); // window
// 拿到foo3返回的普通函数后,通过call绑定到person2中进行调用
person1.foo3().call(person2); // person2
// foo4返回了箭头函数,和自身绑定没有关系,上层找到person1
person1.foo4()(); // person1
/* person1.foo4.call(person2) 导致person1.foo4方法内的this为person2
foo4调用时绑定了person2,返回的函数是箭头函数,调用时,找到了上层绑定的this为person2
*/
person1.foo4.call(person2)(); // person2
// foo4调用返回的箭头函数,和call调用没有关系,找到了上层的this为person1
person1.foo4().call(person2); // person1
例 4:
var name = 'window';
function Person(name) {
this.name = name;
this.obj = {
name: 'obj',
foo1: function () {
return function () {
console.log(this.name);
};
},
foo2: function () {
return () => {
console.log(this.name);
};
},
};
}
var person1 = new Person('person1');
var person2 = new Person('person2');
person1.obj.foo1()();
person1.obj.foo1.call(person2)();
person1.obj.foo1().call(person2);
person1.obj.foo2()();
person1.obj.foo2.call(person2)();
person1.obj.foo2().call(person2);
解析:
// obj.foo1()返回一个函数 这个函数在全局作用域下直接执行(默认绑定window)
person1.obj.foo1()(); // window
/*
最终拿到一个返回的普通函数(虽然多了一步call的绑定)这个普通函数是在全局作用域下直接执行的
所以是默认绑定 window
*/
person1.obj.foo1.call(person2)(); // window
// 拿到返回的普通函数,在全局作用域下调用,但是有用call显绑定this为person2 所以this为person2
person1.obj.foo1().call(person2); // person2
// 拿到foo2()的返回值,是一个箭头函数 箭头函数在执行时找上层作用域下的this,就是obj
person1.obj.foo2()(); // obj
/* foo2()的返回值依然是箭头函数,但是在执行foo2函数时显式绑定了this为person2,所以箭头函数在执行时
找到执行时的上层作用域下的this,找到的person2 */
person1.obj.foo2.call(person2)(); // person2
// foo2()的返回值,依然是箭头函数
// 箭头函数通过call调用是不会显式绑定this,所以找上层作用域下的this式式obj
person1.obj.foo2().call(person2); // obj