javascript在词法环境中注册标识符

60 篇文章 0 订阅
55 篇文章 1 订阅

JavaScript作为一门编程语言,其设计的基本原则是易用性。这也是不需要指定函数返回值类型、函数参数类型、变量类型等主要原因。你已经了解到JavaScript是逐行执行的。

查看代码如下:

firstRonin = 'Kiyokawa';

secondRoin = 'Knodo';





将Kiyokawa赋值给firstRonin,将Knodo赋值给标识给标识符secondRoin。对比下面代码:

console.log("------------------------在词法环境中注册标识符--------------------");

const firstRoin = 'Kiyokawa';

check(firstRoin);

function check(ronin) {

if (ronin === 'Kiyokawa') {

console.log("The ronin was checked!");

}

}

 

在上面的代码中,我们将Kiyokawa赋值给firstRoin,然后调用check函数,传入参数firstRoin。先等一下,如果JavaScript是逐行执行的,我们可以调用check函数吗?函数还没有执行到函数的check的声明,所以javascript引擎不应该认识check函数。

注册标识符的过程

除了易用性,代码逐行执行,JavaScript引擎是如何知道check函数存在的?JavaScript代码的执行事实上是分两个阶段进行的。

一旦创建了新的词法环境,就会执行第一阶段。在第一阶段,没有执行代码,但是JavaScript引擎会访问并注册在当前词法环境中所声明的变量和函数。javascript在第一阶段完成之后,开始第二阶段,具体如何执行取决于变量的类型(let、var、const和函数声明)以及环境类型(全局环境、函数环境或块级作用域)。

具体的处理过程如下:

1.如果是创建一个函数环境,那么创建形参及函数参数的默认值。如果是非函数环境,将跳过此步骤。

2.如果是创建全局或函数环境,就扫描当前代码进行函数声明(不会扫描其他函数的函数体),但是不会执行函数表达式或箭头函数。对于所找到的函数声明,将创建函数,并绑定到当前环境与函数名相同的标识符上。若该标识符已经存在,那么该标识符将被重写。如果是块级作用域,将跳过此步骤。

3.扫描当前代码进行变量声明。在函数或全局环境中,查找所有当前函数以及其他函数之外通过var声明的变量,并查找所有通过let或const定义的变量。在块级环境中,仅查找当前块中通过let或const定义的变量。对于所查找到的变量,若该标识符不存在,进行注册并将其初始化为undefined。若标识符已经存在,将保留其值。

 

整个处理过程如下图所示:

 

 

在函数声明之前调用函数

JavaScript易用性的一个典型特征,函数的声明顺序无关紧要。在JavaScript中,我们可以在函数声明前对其进行调用。

console.log('-----------------------在函数声明之前访问函数----------------');
//若函数是作为函数声明进行定义的,则可以在函数声明之前访问函数
if (typeof fun === 'function') {
  console.log("fun is a function even through its definition is not reached yet!");
}
//若函数是通过函数表达式或箭头函数进行定义的,则不可以在函数定义之前访问函数
if (typeof myFunExp === 'undefined') {
  console.log("But we cannot access function expressions.");
}

if (typeof myArrow === 'undefined') {
  console.log("Nor arrow functions");
}

//作为函数声明进行定义
function fun() {};

//myFunExpr指向函数表达式
var myFunExpr = function () {};

//myArrow指向箭头函数
var myArrow = (x) => x;

 

我们甚至可以在函数定义之前访问函数。我们可以这么做的原因是fun是通过函数声明进行定义的,第二阶段表明函数已经通过声明进行定义,在当前词法环境创建时已在其他代码执行之前注册了函数标识符。所以,在执行函数调用之前,fun函数已经存在。

JavaScript引擎通过这种方式为开发者提供便利,允许我们直接使用函数的引用,而不需要强制指定函数的定义顺序。在代码执行之前,函数已经存在了。

需要注意的是,这种情况仅针对函数声明有效。函数表达式与箭头函数都不在此过程中,而是在程序执行过程中执行定义的。这就是不能访问myFunExp与myArrow函数的原因。

函数重载

第二个难题是处理重载函数标识符的问题。
console.log('---------------------重载函数标识符--------------------');
//fun指向一个函数
if (typeof fun === 'function') {
  console.log('We access the function.');
}
//定义变量fun并赋值为数字3
var fun = 3;
//fun指向一个数字
if (typeof fun === 'number') {
  console.log('Now we access the number.');
}
//函数声明
function fun() {}

//fun仍然指向数字
if (typeof fun === 'number') {
  console.log('Still a number.');
}

 

 

上述代码中,声明的变量与函数均使用相同的名字fun。通过Log发现:三个if语句都通过了,第一个if语句中,标识符fun指向一个函数。第二个if语句中,标识符fun指向一个数字。

JavaScript的这种行为是由标识符注册的结果直接导致的。通过函数声明进行定义的函数在代码执行之前对函数进行创建,并赋值给对应的标识符;在第3步中,处理变量的声明,那些在当前环境中未声明的变量,将被赋值为undefined。

在上述代码中,注册函数声明时,由于标识符fun已经存在,并且未被赋值给undefined。这就是第1个测试fun是否是函数的if语句执行通过的原因。

之后,执行赋值语句var fun = 3,将数字3赋值给标识符fun。执行完这个赋值语句后,fun函数就不再指向函数了,而是指向数字3。

在程序的实际执行过程中,跳过了函数声明部分,所以函数的声明不会影响标识符fun的值。

变量提升(variable hoisting)

如果你已阅读关于解释处理标识符的一些JavaScript博客或图书,你可能已经遇到这个词:变量提升。例如,变量的声明提升至函数顶部,函数的声明提升至全局代码顶部。

但是,正如上述实例中看见的,并没有那么简单。变量和函数的声明并没有实际发生移动。只是在代码执行之前,先在词法环境中进行注册。虽然描述为提升了,并且进行了定义,这样更容易理解JavaScript的作用域的工作原理,但是,我们可以通过词法环境对整个处理过程进行更深入地理解,了解真正的原理。

 

参考《JavaScript忍者秘籍》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值