变量提升
JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值 undefined。
变量提升带来的问题
- 变量容易被覆盖掉
- 本应销毁的变量没有被销毁,例如:
function foo(){
for (var i = 0; i < 7; i++) {
}
console.log(i);
}
foo()
在 for 循环跑完后,i 变量并没有被销毁
如何解决变量提升
利用块级作用域关键字 let 或者 const 关键字替换 var。JavaScript 中有三种作用域,全局作用域、函数作用域以及块级作用域。
- 全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期
- 函数作用域就是在函数内部定义的变量或者函数,并且定义的内容只能在函数内部被访问。函数执行结束后,函数内容就被销毁。
- 块级作用域就是使用一对大括号包裹的一段代码,具体如下
//if块
if(1){}
//while块
while(1){}
//函数块
function foo(){}
//for循环块
for(let i = 0; i<100; i++){}
//单独一个块
{}
JavaScript 代码的执行流程
- 编译阶段
- 将代码编译成两部分内容:
- 执行上下文:JavaScript 执行一段代码时的运行环境,一般来说,有全局执行上下文、函数执行上下文、eval 函数执行上下文三种。执行上下文的内容有 this、方法内置变量、class 内置函数等。
- 可执行代码
- 执行阶段:按照代码顺序与逻辑执行代码
每调用一个函数,JavaScript 引擎会为其创建执行上下文,并把该执行上下文压入调用栈,然后 JavaScript 引擎开始执行函数代码。那么什么是调用栈呢?
调用栈
调用栈是 JavaScript 引擎追踪函数执行的一个机制,当一次有多个函数被调用时,通过调用栈可以追踪到哪个函数正在被执行,以及各函数间的调用关系。
如果在一个函数 A 中调用了另外一个函数 B,那么 JavaScript 引擎会为 B 函数创建执行上下文,并将 B 函数的执行上下文压入栈顶。
当前函数执行完毕后,JavaScript 引擎会将该函数的执行上下文弹出栈。
栈溢出(Stack Overflow)
调用栈是一种管理执行上下文的数据结构,符合后进先出的规则。当入栈的执行上下文超过一定数目,JavaScript 引擎就会报错,我们把这种错误叫做栈溢出。
栈溢出一般都是通过多次递归调用,或者函数间相互调用造成的。
如何解决栈溢出
修改逻辑降低递归次数,改写递归为循环等
作用域链
每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部执行上下文。当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量,当前的执行上下文查找不到变量再去外部执行上下文中查找。这个查找的链条就被称为作用域链。
注意,作用域链遵照词法作用域,词法作用域是指,作用域由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能预测代码在执行过程中如何查找标识符。
function foo() { console.log(a) } function bar() { let a = 1 foo() } let a = 2 bar() // 2上面的代码中 foo 是在全局作用域下声明的,不管在哪里执行 foo 函数,它的外部引用都是全局作用域。
闭包
在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。
如何回收闭包
如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。
如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。
所以在使用闭包的时候,你要尽量注意一个原则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。
this 指针
JavaScript 中的 this 是和执行上下文绑定的,每个执行上下文中都有一个 this,this还可以分为两种:
- 全局执行上下文中的 this:指向 window 对象,不可修改。
- 函数执行上下文中的 this:默认情况下指向 window 对象,但是可以通过以下三种方式来设置 this 值。
- 通过函数的 call、bind 和 apply 方法
2.通过对象调用方法,使用对象来调用其内部的一个方法,该方法的 this 是指向对象本身的let bar = { myName : "极客邦", test1 : 1 } function foo(){ this.myName = "极客时间" } foo.call(bar) console.log(bar) console.log(myName)3.通过构造函数来设置var myObj = { name : "极客时间", showThis: function(){ console.log(this) } } myObj.showThis()当执行 new CreateObj() 的时候,JavaScript 引擎做了如下四件事:function CreateObj(){ this.name = "极客时间" } var myObj = new CreateObj()- 首先创建了一个空对象 tempObj;
- 接着调用 CreateObj.call 方法,并将 tempObj 作为 call 方法的参数,这样当 CreateObj 的执行上下文创建时,它的 this 就指向了 tempObj 对象;
- 然后执行 CreateObj 函数,此时的 CreateObj 函数执行上下文中的 this 指向了 tempObj 对象;
- 最后返回 tempObj 对象。
this 的设计缺陷以及应对办法
嵌套函数中的 this 不会从外层函数中继承
var myObj = {
name : "极客时间",
showThis: function(){
console.log(this)
function bar(){console.log(this)}
bar()
}
}
myObj.showThis()
这段代码中 bar 的打印结果是 window,并没有继承外层函数设置的 myObj
可以使用 ES6 中的箭头函数来解决这个问题。
var myObj = {
name : "极客时间",
showThis: function(){
console.log(this)
var bar = ()=>{
this.name = "极客邦"
console.log(this)
}
bar()
}
}
myObj.showThis()
console.log(myObj.name)
console.log(window.name)