最近看了一些javascript相关的知识,打算整理一下
一、作用域
先来看一段代码
for (var i = 0; i < 10; i ++) {
console.log('test');
}
console.log(i);
如果是在其他语言里面,console.log(i)会报错,因为i的只在for循环这个块里面起作用。但是javascript里面这段代码打印结果是10
。
下面说一下javascript里面的作用域以及作用域链等相关知识
1.1 全局作用域 和 函数作用域
在javascript里,作用域
是用来规定变量与函数可见性和生命周期一套规则。在ES6之前,作用域有两种,分别是全局作用域
与函数作用域
。
全局作用域
中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
函数作用域
即函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。
看下面这段示例代码:
var a = 3;
function foo () {
console.log(a);
if (1) {
var a = 5;
console.log(a);
}
console.log(a);
}
foo();
这段示例代码的打印结果是:
undefined
5
5
按照其他语言里面的逻辑,照理说第一个console.log(a)应该打印出3,但是在javascript里面,因为函数作用域以及变量提升的存在,所以第一个console.log(a)打印出了undefined
1.2 模拟块级作用域
在ES5里面可以利用函数来达到模拟块级作用域的目的,例如对最上方的for循环代码进行改造,看以下示例代码:
(function () {
for (var i = 0; i < 10; i ++) {
console.log('test');
}
})();
console.log(i);
这段代码打印结果是:
Error: i is not defined
这种方式叫函数自执行
,以下这种方式是最常用的写法。
(functino() {
……
})();
1.3 let 与 const关键字
ES6引入了let和const关键字,使javascript也能和其他语言一样拥有块级作用域。
使用let
关键字声明的变量是可以被改变的,使用const
关键字声明的变量是不可以被改变的。使用let/const命令声明的变量只在let/const命令所在代码块内有效。
使用let关键字来改造上方的代码,示例代码如下
var a = 3;
function foo () {
console.log(a);
if (1) {
let a = 5;
console.log(a);
}
console.log(a);
}
foo();
以上这段示例代码打印出:
3
5
3
这样就实现了块内声明的变量不影响块外面的变量。
1.4 javascript是如何支持块级作用域的
块级作用域是通过词法环境的栈结构来实现的。(变量提升是通过变量环境来实现的)词法环境与变量环境结合,使得javascript同时支持变量提升和块级作用域。
下面以代码具体来说明执行上下文里面变量环境和词法环境的变化。
function foo() {
var a = 1
let b = 2
{
let b = 3
var c = 4
const d = 5
console.log(a)
console.log(b)
console.log(c)
console.log(d)
}
console.log(a)
console.log(b)
console.log(c)
}
foo();
这段代码的打印结果是:
1
3
4
5
1
2
4
在foo函数的执行上下文刚被创建的时候,如下图所示,函数内部使用var声明的变量,在编译阶段被放到了变量环境中,使用let/const声明的变量,在编译阶段被放到了词法环境中。在函数内部的块作用域内,使用let/const声明的变量没有被放到词法环境中。
当执行到块作用域时,执行上下文如下图所示
在执行到块作用域的console.log(a)时,执行上下文如下图所示。此时需要查找a,先在词法环境中从栈顶往栈底查找a,找不到再去变量环境中查找
当块作用域执行结束之后,其内部定义的变量就会从词法环境的栈顶弹出,如下图所示
注意: 在块作用域内,使用let/const声明的变量被提升,只是变量的创建被提升,初始化并没有被提升。看以下示例代码:
let a = 1
{
console.log(a)
let a = 2
}
打印结果是:
Error: Cannot access 'a' before initialization
二、作用域链
先看一段示例代码,看看打印结果是不是符合想象
function bar() {
console.log(a)
}
function foo() {
var a = 10
bar()
}
var a = 5
foo()
这段代码的打印结果是5
5
本来猜测打印结果是10,但是实际上打印结果是5,这里就涉及到作用域链
相关知识。
在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文。在查找变量的时候,会首先在当前执行上下文
中查找,找不到的话,再去外部执行上下文中查找。这个查找的链条就称为作用域链
。
而上述代码中,bar函数执行上下文的外部引用为全局执行上下文
,foo函数执行上下文的外部引用也为全局执行上下文
。外部引用的指向是由词法作用域
决定的。
词法作用域
是指作用域是有代码中函数声明的位置来决定的,是静态的作用域。词法作用域
是代码阶段就决定好的,和函数是怎么调用的没有关系。根据代码中的位置,bar函数和foo函数的外部引用都是全局执行上下文
。
同一个作用域下,对同一个函数的不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值,所以,作用域中变量的值是在执行过程中确定的,而作用域是在函数创建时就确定的。如果要查找一个作用域下某个变量的值,就需要找到这个作用域对应的执行上下文环境,再在其中找到变量的值
如果上方的代码改成下面这个样子,那么打印结果就不同了,如下方所示:
function foo() {
function bar() {
console.log(a)
}
var a = 10
bar()
}
var a = 5
foo()
这段代码的打印结果是
10
关于块级作用域中变量的查找,可以看下方示例代码:
function fun1() {
function fun2() {
var a = 1
let c = 100
if (1) {
let a = 2
console.log(b)
}
}
var a = 3
let b = 50
{
let b = 60
fun2()
console.log(b)
}
}
var a = 4
let b = 80
fun1()
打印结果是:
50
60
为什么打印出来的第一个是50而不是60呢,因为作用域在代码阶段就决定了,fun2里面使用了自由变量b,所以找不到的时候要去定义fun2的fun1的作用域里面找,而在fun1的作用域里面,只有值为50的那个b的作用域才是fun1,值为60的那个b的作用域只是一个块。
如果想要打印结果是两个60,则可以将代码改成这样:
function fun1() {
var a = 3
let b = 50
{
function fun2() {
var a = 1
let c = 100
if (1) {
let a = 2
console.log(b)
}
}
let b = 60
fun2()
console.log(b)
}
}
var a = 4
let b = 80
fun1()