最近在一个前端学习群里,有人抛出了这么一道 JS 面试题。javascript
var foo = 1; (function foo(){ foo = 100; console.log(foo); }()) console.log(foo);
我一看,这不很简单吗?IIFE 局部的 foo
原本指向函数自己,但后来被修改为 100 了,因此局部的 foo
打印 100。全局的 foo
仍是保留原来的值,因此全局的 foo
打印 1。html
而后我复制代码到控制台运行,发现先打印函数体 foo(){...}
,而后再打印 1
。前端
我猜测的第一个打印结果错了,一番查找资料终于搞懂了,因而有了这篇文章。java
如下表示形式的是函数声明式,简单说就是 function
前面没有任何运算符,其实就下面一种形式。git
function name() { ... }
如下表示形式的是函数表达式,有多种形式。github
var fun = function name() { ... } // 函数前带有 + - * / () && || 等运算符号 (function name(){ ... }()) // 又或者 +function name(){ ... }()
能认出函数声明式与函数表达式后,咱们来看看二者有什么区别。面试
稍微了解 JS 的都知道变量提高(variable hoisting),除此以外还有函数提高(function hoisting),也就是说下面的代码是正常运行的。express
foo(); // running function foo() { console.log('running'); }
可是函数提高只对函数声明式有效,对函数表达式不生效,下面的代码就会报错。ide
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 绑定在函数内部的做用域下。
说了这么多,好像还没说的真正的缘由。是的,前面的内容只是铺垫,有了上面的内容,才能更好理解背后的缘由。
解释前先说缘由:
缘由出自《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 面试题