资讯专栏INFORMATION COLUMN

【理解】一道 JS 面试题

binaryTree / 2519人阅读

摘要:最近在一个前端学习群里,有人抛出了这么一道面试题。以下表示形式的是函数表达式,有多种形式。函数声明式的函数名是可修改的。重新声明变量通过上面的分析解释,希望你可以掌握这道面试题,举一反三。原文链接理解一道面试题

最近在一个前端学习群里,有人抛出了这么一道 JS 面试题。

var foo = 1;
(function foo(){
    foo = 100;
    console.log(foo);
}())
console.log(foo);

我一看,这不很简单吗?IIFE 局部的 foo 本来指向函数本身,但后来被修改成 100 了,所以局部的 foo 打印 100。全局的 foo 还是保留原来的值,所以全局的 foo 打印 1。

然后我复制代码到控制台运行,发现先打印函数体 foo(){...},然后再打印 1

我猜想的第一个打印结果错了,一番查找资料终于搞懂了,于是有了这篇文章。

函数声明式 vs 函数表达式

以下表示形式的是函数声明式,简单说就是 function 前面没有任何运算符,其实就下面一种形式。

function name() {
    ...
}

以下表示形式的是函数表达式,有多种形式。

var fun = function name() {
    ...
}

// 函数前带有 + - * / () && || 等运算符号
(function name(){
    ...
}())

// 又或者
+function name(){
    ...
}()

能认出函数声明式与函数表达式后,我们来看看两者有什么区别。

函数提升

稍微了解 JS 的都知道变量提升(variable hoisting),除此之外还有函数提升(function hoisting),也就是说下面的代码是正常运行的。

foo(); // running

function foo() {
    console.log("running");
}

但是函数提升只对函数声明式有效,对函数表达式不生效,下面的代码就会报错。

foo(); // Uncaught TypeError: foo is not a function

var foo = function () {
    console.log("running");
}

区别一:函数声明式会提升函数定义,而函数表达式不提升函数定义。这一区别只是想给大家复习知识点,并不是本文的重点。

函数名绑定的作用域

先看看下面函数的表示形式,记住它有助于接下来的说明。

function BindingIdentifier (FormalParameters) { FunctionBody }

函数声明式和函数表达式的另外一个关键区别是,看函数名(BindingIdentifier)绑定到哪个作用域下。

先看下 ECMAScript 是怎么描述这一区别的。

The BindingIdentifier in a FunctionExpression can be referenced from inside the FunctionExpression"s FunctionBody to allow the function to call itself recursively. However, unlike in a FunctionDeclaration, the BindingIdentifier in a FunctionExpression cannot be referenced from and does not affect the scope enclosing the FunctionExpression.

上面说 BindingIdentifier(函数的引用) 可以用于在函数表达式内递归调用自身。而且函数表达式的 BindingIdentifier 只绑定在该函数内部,不污染外部的作用域,外部作用域也无法访问到 BindingIdentifier。

区别二:函数声明式的 BindingIdentifier 绑定在声明时的作用域下,函数表达式的 BindingIdentifier 绑定在函数内部的作用域下

背后的原因

说了这么多,好像还没说的真正的原因。是的,前面的内容只是铺垫,有了上面的内容,才能更好理解背后的原因。

解释前先说原因:

函数表达式的函数名是不可修改的(ImmutableBinding)。但如果你真修改了,在非严格模式下会静默失败,在严格模式下会报错(Uncaught TypeError: Assignment to constant variable)。

函数声明式的函数名是可修改的(MutableBinding)。

原因出自《You-Dont-Know-JS》的一个 issue,这一 issue 已被作者纳入第二版(second edition)的编写中。

The production FunctionExpression : function Identifier ( FormalParameterListopt ) { FunctionBody } is evaluated as follows: ... Call the CreateImmutableBinding concrete method of envRec passing the String value of Identifier as the argument. ...

调用 CreateImmutableBinding 创建 Immutable"s 函数名。

For each FunctionDeclaration f in code, in source text order do ... If funcAlreadyDeclared is false, call env’s CreateMutableBinding concrete method passing fn and configurableBindings as the arguments. ...

调用 CreateMutableBinding 创建 Mutable"s 函数名。

当然也可以从 ECMAScript 规范中找到原因:Runtime Semantics: Evaluation。

至于语言为什么要这么规定,我也没想明白,如果有知道的同学可以分享一下。

分析下代码

那回头再分析下一开始的示例,从每一行注释可以帮助理解背后的原因。

var foo = 1; // 在外部作用域声明foo=1

// IIFE是典型的函数表达式
(function foo(){ // 函数名foo,引用函数自身,绑定在函数内部,不污染外部作用域
    foo = 100; // 这里修改了foo,但规范规定不能修改,但不会报错
    console.log(foo); // 还是引用函数自身
}())

console.log(foo); // 外部作用域一直是1

同样的代码,当函数运行在严格模式下,报错提示说:“不能赋值给常量”。也就是说函数表达式的函数名被定义成常量,无法再修改了。

var foo = 1;

(function foo(){
    "use strict"; // 严格模式
    foo = 100; // Uncaught TypeError: Assignment to constant variable
    console.log(foo);
}())

console.log(foo);

为了帮助对比理解,下面给出了函数声明式的示例及解释,下面的代码无论在非严格模式还是严格模式下都打印100,也就是说函数声明式的函数名可以被修改。

// foo是函数声明式
function foo(){ // 函数名foo,引用函数自身,绑定在声明时的作用域下
    foo = 100; // 修改了foo,函数声明式内可以重新修改函数名
    console.log(foo); // 100
}
foo();

如果在函数表达式内使用 var foo = 100; 来重新声明变量,那这个变量就不是不可修改的(ImmutableBinding),所以内部的 foo 打印 100。

var foo = 1;

(function foo(){
    var foo = 100; // 重新声明变量
    console.log(foo); // 100
}())

console.log(foo); // 1

通过上面的分析解释,希望你可以掌握这道面试题,举一反三。

如果你喜欢这篇文章,请关注我,我会持续输出更多原创且高质量的内容。

原文链接:【理解】一道 JS 面试题

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/103665.html

相关文章

  • 一道JS面试引发的血案

    摘要:项目组长给我看了一道面试别人的面试题。打铁趁热,再来一道题来加深下理解。作者以乐之名本文原创,有不当的地方欢迎指出。 showImg(https://segmentfault.com/img/bVbur0z?w=600&h=400); 刚入职新公司,属于公司萌新一枚,一天下午对着屏幕看代码架构时。BI项目组长给我看了一道面试别人的JS面试题。 虽然答对了,但把理由说错了,照样不及格。 ...

    fantix 评论0 收藏0
  • JavaScript中的map方法,以及一道js面试

    摘要:今天看见一道面试题答案是多少答案是对方法不太了解就去搜了一下,里面也包含了对这道面试题的详解。方法返回一个由原数组中的每个元素调用一个指定方法后返回值组成的新数组。使用方法处理数组时,数组元素的范围在方法第一次调用之前就已经确定了。 今天看见一道面试题:[1,2,3].map(parseInt)答案是多少?答案是[1,NaN,NaN] 对map()方法不太了解就去搜了一下:Array....

    LittleLiByte 评论0 收藏0
  • AMD的一道面试

    摘要:之前的面试中,一直感觉模块化没有什么可以问的,不过昨天面试突然想到一个题目对于一个的模式下文件如下很多代码很多代码很多代码文件分别是什么时候加载的,如何加载的题目不难答案是和是在加载完后就加载。 showImg(https://segmentfault.com/img/bVseuJ); 模块化现在应该已经成为了稍微复杂一点前端开发的标配了。在es6中,都已经支持了的模块化。 之前的...

    xiaoqibTn 评论0 收藏0
  • 一道面试引发的思考 --- Event Loop

    摘要:想必面试题刷的多的同学对下面这道题目不陌生,能够立即回答出输出个,可是你真的懂为什么吗为什么是输出为什么是输出个这两个问题在我脑边萦绕。同步任务都好理解,一个执行完执行下一个。本文只是我对这道面试题的一点思考,有误的地方望批评指正。 想必面试题刷的多的同学对下面这道题目不陌生,能够立即回答出输出10个10,可是你真的懂为什么吗?为什么是输出10?为什么是输出10个10?这两个问题在我脑...

    betacat 评论0 收藏0
  • 一道面试引发的思考

    摘要:下面我们来使用面向对象类图这里就不再画了首先面试题中所提到的我们都可以看成类,比如停车场是一个类吧,它里面的车位是一个类吧,摄像头,屏幕。。。 以下是某场的一道面试题(大概): 1、一个停车场,车辆入场时,摄像头记录下车辆信息2、屏幕上显示所接收的车辆的信息情况(车牌号)以及各层车位的车位余量3、停车场一共四层车位,其中的三层都为普通车位,还有一层为特殊车位(体现在停车计费价格上面的不...

    Apollo 评论0 收藏0

发表评论

0条评论

binaryTree

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<