本篇将从JS的执行上下文开始,去理解:变量提升、 栈式调用、作用域和闭包。
参考:
浏览器工作原理与实践
JS执行上下文
执行上下文是 JavaScript 执行一段代码时的运行环境,比如调用一个函数,就会生成这个函数的执行上下文,确定该函数在执行期间用到的诸如 this、变量环境、词法环境、外部环境引用等。
JS引擎执行JS代码分为两部分,第一部分是编译、第二部分才是执行,在编译阶段,会做变量提升等操作,生成JS执行上下文。
JS执行上下文中包含变量环境,变量提升的变量都存储在这里面,包括用户声明的变量以及函数声明变量,里面还包括指向外部变量环境的引用(作用域链查找)、词法环境,存储块级变量声明,即const\let声明的量,在生成了执行上下文后,才会继续执行JS可执行的代码。
执行时,需要从作用域中查询某个变量,具体查询办法是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。
showName()
console.log(myname)
showName1()var myname = '!!!'
function showName() {console.log('函数showName被执行');
}
var showName1 = function () {console.log('函数showName1被执行');
}// 输出
函数showName被执行
undefined
Error:showName1 is not a function如果showName1声明 var改成const Error:showName1 is not defined
说明:
变量的生成涉及到 声明、赋值两个阶段。在声明阶段JS引擎会把变量和函数的声明提升到代码开头执行,因此showName可以被执行,myname被默认赋值为undefined,但是showName1是一个变量赋值,它一开始只是提升了声明,因此showName1是undefined,会报错not a function。
var的创建和初始化被提升,赋值不会提升。
let的创建被提升,初始化和赋值不会提升。
function的创建、初始化和赋值都被提升。
变量提升,是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。变量提升操作并不会直接在代码里显现出来,它是在JS代码的编译阶段完成的。
为什么要引入块级作用域
因为会存在着变量覆盖、无法自动回收全局变量导致占用内存 等设计缺陷,所以 ES6 引入了块级作用域关键字来解决这些问题。
作用域
作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。
作用域在编码时就确定了, 不会再变化,可以用于隔离变量, 在不同作用域定义同名的变量不冲突。
作用域链:多个嵌套的作用域形成的由内向外的结构, 用于查找变量
分类:
- 全局作用域:全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
- 函数作用域:函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。
- 块级作用域。
区别作用域与执行上下文
作用域: 静态的, 编码时就确定了(不是在运行时), 一旦确定就不会变化了
执行上下文: 执行代码时在编译阶段动态创建, 当执行结束消失
联系: 执行上下文环境是在对应的作用域中的
作用域链
function bar() {console.log(myName)
}
function foo() {var myName = "极客邦"bar()
}
var myName = "极客时间"
foo()
答案是会输出:极客时间。
为什么bar查询的作用域链不会指向foo的作用域链,而是指向全局?
因为作用域链的生成由函数声明的位置决定,和调用位置、调用栈无关。
要回答这个问题,你还需要知道什么是词法作用域。
这是因为在 JavaScript 执行过程中,其作用域链是由词法作用域决定的。
词法作用域词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。
块级作用域
function bar() {var myName = "极客世界"let test1 = 100if (1) {let myName = "Chrome浏览器"console.log(test)}
}
function foo() {var myName = "极客邦"let test = 2{let test = 3bar()}
}
var myName = "极客时间"
let myAge = 10
let test = 1
foo()
分析:
顺序由图中所示
可以由图里知道,一个变量 作用域链会先查词法环境、再查变量环境,接下来顺着外部引用,依次查询词法环境、变量环境。
因此会输出:1