什么是变量提高?数组
变量提高是指在 JS 代码的执行过程当中,JavaScript 引擎把变量和函数的声明部分提高到代码开头的行为。浏览器
为何会有变量提高?闭包
ES6 以前的 JS 没有块级做用域,因此把做用域内部的变量统一提高是最快速、最简单的设计。app
变量提高带来的问题函数
(1) 变量容易在不被察觉的状况下被覆盖掉;工具
(2) 本应销毁的变量没有被销毁。性能
同名变量和函数的两点处理原则优化
(1) 若是是同名的函数,JavaScript 编译阶段会选择最后声明的那个;ui
(2) 若是变量和函数同名,那么在编译阶段,变量的声明会被忽略。执行阶段变量正常赋值。this
console.log(foo) // 执行结果为:f foo() {},说明变量foo的声明被忽略了
function foo() {}
var foo = 2;
console.log(foo) // 执行结果为:2,说明变量foo的赋值被执行了
复制代码
如何解决变量提高带来的问题
ES6 引入let
,让 JavaScript 也有块级做用域。
在同一段代码中,ES6 是如何作到既支持变量提高又支持块级做用域的?
JS 代码在执行过程当中会经历编译和执行两个过程。
var
声明的变量,被放到执行上下文的变量环境中,经过let
声明的变量,被放到执行上下文的词法环境中。块级做用域中经过let
声明的变量没有被放到词法环境。let
声明的变量,被存放到当前执行上下文的词法环境中一个单独的区域,这个区域并不影响块级做用域外面的变量,即这个区域和区域外的同名变量是独立的存在。什么是变量环境和词法环境?
变量环境和词法环境都是执行上下文中定义的对象。变量环境对象保存的是变量提高的内容,词法环境对象保存的是经过let
或const
声明的变量。
什么是执行上下文?
JS 代码在编译的时候会生成执行上下文和可执行代码。执行上下文是 JavaScript 执行一段代码时的环境,其中存在一个变量环境的对象,该对象保存了变量和函数的声明部分。
什么状况下会建立执行上下文?
eval
函数的时候,eval
的代码也会被编译,并建立执行上下文。如何管理执行上下文?
JavaScript 用调用栈管理上下文。
什么是调用栈?
调用栈是管理执行上下文的栈,是 JavaScript 引擎追踪函数执行的一个机制,经过调用栈能够追踪到哪一个函数正在被执行以及各函数之间的调用关系。
代码执行过程当中,调用栈是如何变化的?
(1) 首先建立全局上下文,并将其压入栈底,而后执行全局上下文。
(2) 执行到函数调用代码时,编译被调用的函数,为其建立一个执行上下文,并将执行上下文压入栈中。而后执行函数代码。
(3) 再次执行到函数调用代码时,重复第 (2) 步。
(4) 栈顶函数执行完后,其执行上下文从栈顶弹出,并将函数的返回值赋给调用函数的相应变量。
(5) 继续执行栈顶函数,并重复第 (4) 步,直至全局代码执行完毕。
如何在浏览器中查看调用栈的信息?
以 Chrome 浏览器为例,在开发者工具的"Sources"
工具栏中,给指定代码加上断点,代码执行到断点处时暂停执行,在右侧的"Call Stack"
中查看当前的调用栈。
另外一种方法是,在函数代码中加上console.trace()
语句,而后在控制台查看输出结果。
什么是栈溢出?什么状况下会出现栈溢出?
调用栈是有大小的,当入栈的执行上下文超过必定数目时,JavaScript 引擎就会报栈溢出的错误:
Uncaught RangeError: Maximum call stack size exceeded
复制代码
没有终止条件的递归函数,会一直建立新的执行上下文,并反复将执行上下文压入调用栈中,最终超出栈的容量限制,致使栈溢出。
什么是做用域?
做用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,做用域就是变量与函数的可访问范围,即做用域控制着变量和函数的可见性和生命周期。
有哪几种做用域?
ES6 以前,只有两种做用域:全局做用域和函数做用域。ES6 增长了块级做用域。
(1) 全局做用域中的对象在代码中的任何地方都能被访问,其生命周期伴随着页面的生命周期。
(2) 函数做用域就是在函数内部定义的变量或函数,而且定义的变量或者函数只能在函数内部被访问。函数执行结束以后,函数内部定义的变量会被销毁。
(3) 块级做用域是在大括号内代码块中使用let
或const
声明的变量,变量只能在代码块内被访问。代码块执行结束后,代码块内定义的变量会被销毁。
什么是做用域链?
每一个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文。当一段代码使用了一个变量时,JavaScript 引擎首先会在当前的执行上下文中查找该变量,若是没有找到,就继续在外部引用所指向的执行上下文中查找。这个查找的链条就是做用域。
变量的具体查找方式?
在当前执行上下文中,沿着词法环境的栈顶向下查询,若是找到了变量,就直接返回给 JavaScript 引擎,若是没有找到,就继续在变量环境中查找,找到了就返回,没找到就在当前执行上下文的外部引用所指向的执行上下文中查找。
为何函数的执行上下文的外部引用不是调用它的函数的执行上下文?
由于在 JavaScript 执行过程当中,其做用域链是由词法做用域决定的。词法做用域是指做用域是由代码中函数声明的位置来决定的,因此词法做用域是静态的做用域,在代码阶段就决定好了,和函数是怎么调用的没有关系。经过词法做用域可以预测代码在执行过程当中如何查找标识符。
什么是闭包?
在 JavaScript 中,根据词法做用域的规则,内部函数老是能够访问其外部函数中声明的变量,当经过调用一个外部函数返回一个内部函数后,即便外部函数已经执行结束了,内部函数引用外部函数的变量也依然保存在内存中,这些变量的集合称为闭包。
如何在浏览器中查看闭包?
打开 Chrome 的开发者工具,在使用闭包的函数的任意地方打上断点,而后刷新页面,代码执行到断点处时,能够在右侧"Scope"
中的"Closure"
查看闭包。
闭包中变量的查找方式?
先在当前执行上下文中查找,若是没有找到,就查找外部函数的闭包,仍没有找到就去外部函数的外部引用所指向的执行上下文中查找。体如今"Scope"
中,这个查找过程就是:Local -> Closure -> Global
。
闭包是怎么回收的?
一般,若是引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但若是这个闭包之后再也不使用的话,就会形成内存泄漏。若是引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容若是已经再也不被使用了,垃圾回收器就会回收这块内存。
闭包的应用场景
一般使用只有一个方法的对象的地方,均可以使用闭包。
(1) 为响应事件而执行的函数。
function makeSizer(size) {
return function() {
document.body.style.fontSize = size + 'px';
};
}
document.getElementById('size-12').onclick = makeSizer(12);
document.getElementById('size-14').onclick = makeSizer(14);
复制代码
(2) 用闭包模拟私有方法:用闭包定义公共函数,并令其能够访问私有函数和变量。这个方式称为“模块模式”。
var makeCounter = function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
document: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
}
var Counter1 = makeCounter();
// Counter1和Counter2指向两个不一样的对象,在一个闭包内对变量的修改,不会影响到另一个闭包中的变量
var Counter2 = makeCounter();
var Counter3 = Counter1; // Counter3和Counter1指向同一个对象,会相互影响
复制代码
(3) 解决循环中使用var
定义变量形成的常见错误。
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = {};
a[i].log = function() {
console.log(i);
}
}
a[2].log(); // 10
复制代码
问题:调用数组a
任何一个元素的log
方法,都打印10。
缘由:数组a
中每一个元素中的log
属性的值都为function() { console.log(i) }
,for循环执行结束后,i
没有被销毁,值为10,因此打印的结果都是10。
解决方案:
增长一个闭包
var a = [];
function set(x) {
return function() {
console.log(x);
}
}
for (var i = 0; i < 10; i++) {
a[i] = {};
a[i].log = set(i);
}
a[2].log() // 2
复制代码
原理:能够理解为闭包帮忙保存了每次执行循环时当前的i
值。
a
中第i
个元素的log
属性值为set(i)
函数的返回值;set
函数编译时,变量x
进入set
函数的执行上下文,执行时,x
被赋值为传入的i
;set
函数执行完后,其执行上下文从调用栈顶部弹出,但闭包中的变量x
依然保存在内存中,a[i]
被赋值为set
函数返回的函数function() { console.log(x) }
;log
属性被赋值时,都会从新编译并执行set
函数,生成新的函数执行上下文,函数执行完后,其执行上下文被销毁,但被赋值为当前i
值的x
都继续保存在内存中,因此每一个元素的log
方法所”拥有“的x
值都不同。使用当即执行的匿名函数
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = {};
a[i].log = function(num) {
return function() {
console.log(num);
}
}(i)
}
a[2].log() // 2
复制代码
原理:和第一种方案相同。
使用let
关键字
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = {};
a[i].log = function() {
console.log(i);
}
}
a[2].log() // 2
复制代码
原理:花括号加let
关键字造成了块级做用域。数组a
的每一个元素的log
方法的[[Scopes]]
下都有一个Block
属性保存块级做用域的值,以下图所示。
使用var
关键字的结果以下图所示。
合理使用闭包
若是不是某些特定任务须要使用闭包,不建议在其余函数中建立函数,由于闭包在处理速度和内存消耗方面对脚本性能具备负面影响。
例如,在建立新的对象或者类型时,方法一般应该关联于对象的原型,而不是定义到对象的构造函数中。缘由是后者会致使每次构造函数被调用时,方法都会被从新赋值一次。
this
指向JavaScript 中为何会出现this
?
JavaScript 的做用域机制不支持在对象内部的方法中使用对象内部的属性,因此使用this
来解决这个问题。
this
是什么?
this
是和执行上下文绑定的,每一个执行上下文都有一个this
。
结合上文所述,执行上下文中包含:变量环境、词法环境、outer
(当前执行上下文指向外部执行上下文的引用)、this
。
不一样执行上下文中的this
(1) 全局上下文中的this
:指向window
对象。这也是this
和做用域链的惟一交点,做用域链的最低端包含了window
对象,全局执行上下文中的this
也是指向window
对象。
(2) 函数执行上下文中的this
:
经过函数的call
方法设置的this
:调用call
方法的函数中的this
指向了call
方法传入的第一个参数。也能够经过apply
、bind
方法设置函数执行上下文的this
。
let bar = {
myName: 'Helena'
}
function foo() {
this.myName = 'Wucan'
}
foo.call(bar)
console.log(bar.myName) // Wucan
复制代码
经过对象调用方法设置this
:使用对象来调用其内部的一个方法,该方法的this
是指向对象自己的。
var myObj = {
name: 'Helena',
showThis: function() {
console.log(this.name);
}
}
myObj.showThis() // Helena
myObj.showThis.call(myObj) // Helena
var foo = myObj.showThis;
foo() // undefined,this指向了全局对象
复制代码
结论:
this
指向的是全局变量window
。this
指向对象自己。在构造函数中设置this
function Person() {
this.name = 'name'
}
var p = new Person()
复制代码
经过new
和构造函数建立的对象中的this
指向对象自己。
this
的设计缺陷及应对方案?
(1) 嵌套函数中的this
不会从外层函数中继承
var name = 'Wucan';
var myObj = {
name: 'Helena',
showThis: function() {
console.log(this.name); // Helena,this指向myObj
function bar() {
console.log(this.name); // Wucan,this指向window
}
bar();
}
}
myObj.showThis()
复制代码
解决方案:
this
保存为一个变量,再利用变量的做用域机制传递给嵌套函数。this
。(2) 普通函数中的this
默认指向全局对象window
call
方法改变this
的指向。this
值是undefined
。问题:
下面这段代码会产生栈溢出的问题,如何优化它,以解决栈溢出的问题?
function runStack (n) {
if (n === 0) return 100;
return runStack( n- 2);
}
runStack(50000)
复制代码
试着改成如下代码便可不溢出:
function rs(n) {
while (n !== 0)
n = n - 2;
n = 100;
return n;
}
rs(58579862)
复制代码
想试一下传入多大的参数会出现异常,因而试了下:这个值的临界点有时是58579862,有时是58999998,反正不是个肯定的值。想知道这个值的大小和什么有关系?为何超过了临界值就不会输出结果,也不报错?
下面这段代码,想结合上述概念手动模拟一下编译和执行的整个过程:
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())
复制代码
打断点查看的时候,发现有一个地方与想象中不同。代码执行到断点处时,按照先编译再执行的思路,Local
中应该有四个值为undefined
的变量:myName
、test1
、test2
、innerBar
,实际却以下图所示。test1
是在执行完let test1 = 1
后出如今Local
中且同时被赋值为2的。
觉得是let
和const
声明在编译时仍是有区别的,因而尝试作如下两种修改:
(1) let test1 = 1
改成const test1 = 1
,结果同样;
(2) 将getName()
方法中的console.log(test1)
改成console.log(test2)
,断点执行以下图,test2
变成了test1
:
因此猜测这里是否与闭包中引用了外部函数的变量有关?
参考: