1. 代码执行顺序
先来看下如下代码的执行结果,看是否符合你的预期:
showName()
console.log(myname)
console.log(myage)
var myname = '哈哈'
function showName() {
console.log('showName被执行');
}
结论
- 未声明的变量报错,js执行报错
- 定义前使用不报错,值为undefined, 而非定义的值
- 函数定义前使用,不报错且正常执行
变量提升
声明与赋值
var test = function(){
console.log('test1')
}
function test(){
console.log('test')
}
test()
JS代码执行流程
总结
- JavaScript 代码执行过程中,会发生变量提升,因为 JavaScript 代码在执行之前需要先编译。
- 在编译阶段,变量和函数会被存放到变量环境中,变量的默认值为 undefined;
- 在代码执行阶段,JavaScript 引擎会从变量环境中去查找自定义的变量和函数。
- 如果在编译阶段,存在两个相同的函数,前面的会被覆盖。
2. 块级作用域
作用域
指变量与函数的可访问范围,控制着其可见性及生命周期
变量提升带来的问题
1. 变量被覆盖
var myname = "哈哈"
function showName(){
console.log(myname);
if(0){
var myname = "呵呵"
}
console.log(myname);
}
showName()
2. 变量不及时销毁
function test(){
for (var i = 0; i < 7; i++) {
}
console.log(i);
}
test()
解决方案
使变量仅在代码块内生效(即块级作用域)
使用let、const关键字时浏览器的处理
function test(){
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)
}
test()
执行步骤分析:
- 编译并执行上下文
暂时无法在文档外展示此内容
2. 执行代码
let和const是否存在变量提升?
不存在
总结
- Javascript引擎通过区分变量环境和词法环境,同时支持变量提升和块级作用域
- 变量访问顺序 词法环境栈顶 -> 词法环境栈底 -> 变量环境
3. 作用域链与闭包
function say() {
console.log(myName)
}
function test() {
var c1 = say
var myName = "呵呵"
c1()
}
var myName = "哈哈"
var obj1 = {
a1: 1,
fn1: function () {
console.log(this.a1)
}
}
var obj2 = {
a1: 2,
fn2: obj1.fn1
}
obj2.fn2()
test()
4. 相关概念
调用栈、执行上下文、变量环境、词法环境
每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer。
函数执行上下文使用外部引用的变量,会通过outer向外查找,这个查找链条就是作用域链。
5. js执行过程中的作用域链是如何决定的
由代码中函数声明的位置(词法作用域)决定,词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。
上面代码中,虽然test 函数调用了 say 函数,但是 say 函数的外部引用并不是 test 函数的执行上下文,而是全局执行执行上下文。
function smile() {
var myName = "name1"
let test1 = 100
if (1) {
let myName = "name2"
console.log(test)
}
}
function say() {
var myName = "name3"
let test = 2
{
let test = 3
smile()
}
}
var myName = "name4"
let myAge = 10
let test = 1
say()
变量查找链:当前执行上下文词法环境 -> 当前执行上下文变量环境 -> 外部引用outer词法环境 -> 外部引用outer变量环境
6. 闭包
1. 浏览器如何处理闭包
function test() {
var myName = "name"
let test1 = 1
const test2 = 2
var innerTest = {
getName:function(){
console.log(test1)
return myName
},
setName:function(newName){
myName = newName
}
}
return innerTest
}
var fn = test()
fn.setName("name1")
fn.getName()
console.log(fn.getName())
当执行到 test 函数内部的return innerTest这行代码时调用栈的情况
根据词法作用域的规则,内部函数 getName 和 setName 总是可以访问它们的外部函数 test 中的变量
所以当 innerTest 对象返回给全局变量 fn 时,虽然 test 函数已经执行结束,但是 getName 和 setName 函数依然可以使用 test 函数中的变量 myName 和 test1。所以当 test 函数执行完成之后,执行栈状态如下:
虽然test函数从栈顶弹出,但是会在内存中留下一个包,这个包是test函数的专属包,且只能通过getName 和 setName方法访问,这个包也就是test函数的闭包
在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 test,那么这些变量的集合就称为test 函数的闭包。
2. 闭包的内存模型分析
js如何存储数据
执行流程再分析
- 执行 test 函数,先编译并创建执行上下文。
- 编译过程遇到内部函数setName,JavaScript 引擎还要对内部函数做一次快速的词法扫描,发现该内部函数引用了 test 函数中的 myName 变量,由于是内部函数引用了外部函数的变量,所以 JavaScript 引擎判断这是一个闭包,于是在堆空间创建换一个“closure(test)”的对象(这是一个内部对象,JavaScript 是无法访问的),用来保存 myName 变量。
- 接着继续扫描到 getName 方法时,发现该函数内部还引用变量 test1,于是 JavaScript 引擎又将 test1 添加到“closure(test)”对象中。这时候堆中的“closure(test)”对象中就包含了 myName 和 test1 两个变量了。
- 由于 test2 并没有被内部函数引用,所以 test2 依然保存在调用栈中。
当执行到 test 函数时,闭包就产生了
当 test 函数执行结束之后,返回的 getName 和 setName 方法都引用“clourse(test)”对象,所以即使 test 函数退出了,“clourse(test)”依然被其内部的 getName 和 setName 方法引用。所以在下次调用fn.setName或者fn.getName时,创建的执行上下文中就包含了“clourse(test)”。
产生闭包的核心两步:
第一步是需要预扫描内部函数;第二步是把内部函数引用的外部变量保存到堆中。
闭包是怎么回收的
如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。
如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。
注意:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量