JS函数作用域以及变量提升问题。

xiao252
  • 34

题目描述

function bs(){
    console.log(1);
}
function fn(){
    bs();
    if(false){
        function bs(){
            console.log(2)
        }
    }
}
fn();

自己的思路

对于这一段代码的我的理解是:定义了全局bs、fn函数,而bs函数可忽视,下面调用了fn函数此时fn为独立的作用域,fn函数里面的bs函数我把bs函数看作为var bs=function(){},而它又放在一个if(false)里面,所以这导致它紧紧只是定义了bs变量而已并非赋值函数此时bs变量依然是undefined,上面bs()的调用就出现了bs is not a function

不知道我这样的理解对不对,希望大家指正或补充一下。

回复
阅读 2.1k
5 个回答
✓ 已被采纳

个人认为,解释这个问题就要弄明白 SC(作用域链)VO(变量对象)/ AO(活动对象)

题主的第一个问题:由于函数域存在bs变量,因此覆盖了全局域的bs变量,所以不会输出全局域的bs变量。

function bs(){
  console.log(1)
}
function fn(){
  console.log(bs)
}
fn()

// ƒ bs(){
//   console.log(1)
// }

题主的第二个问题:函数域的时候初始化了VO,所以bs是underfined(此时所有变量都是underfined),但是要执行到if的块级区域才会初始化该块级的AO,因此bs在该块级顶部才是function

function fn() {
  console.log(bs)
  if (true) {
    console.log(bs)
    function bs() {
      console.log(2)
    }
  }
}
fn()

// undefined
// ƒ bs() {
//   console.log(2)
// }

另外刚刚看到评论中提到严格模式,严格模式下是不允许使用未声明变量的,也就是说不能访问SC中的VO。因此访问的是全局变量。此时访问局部变量会报错,但是到if的块级区域下,能够访问AO,实现变量提升。

(function(){
  "use strict"

  function fn(){
    console.log(bs)
    if (true) {
      function bs(){
        console.log(2)
      }
    }
  }

  fn()
})()

// Uncaught ReferenceError: bs is not defined

(function(){
  "use strict"

  function fn(){
    if (true) {
      console.log(bs)
      function bs(){
        console.log(2)
      }
    }
  }

  fn()
})()

// ƒ bs(){
//   console.log(2)
// }

2018-11-04 补充

在其他回答的评论中收到疑问,关于变量提升的问题,题主的疑问核心点是变量提升的背景下的提升情况的问题。

那要先说明代码背景下的变量提升问题:

首先JS一直是没有块级作用域的,其次ES6新增的块级作用域只针对let和const(修修补补用三年的感觉),然后没有写或者var都只有默认的<script>作用域和函数作用域,只有这种情况存在变量提升,也就是提升至这两个域各自的顶部。

问题一:为什么会变量提升?

由于JS是解释即执行的语言,而且是OOP语言,如果一边执行一边修改变量树(SC作用域链)是个非常低效的,因此JS在进入每一个上下文域之后执行代码之前会初始化该域下所有代码,找到var命名及省略命名、函数命名,并把变量加入变量树,意思是告诉后面的执行语句,你有什么变量可以使用。此时在域顶部访问变量会输出underfined。也就是著名的变量提升。

Scope = AO&VO + [[Scope]]其中,AO始终在Scope的最前端。

此时回答题主关于变量提升的背景情况:控制台报错is not function而不是is not defined说明变量确实提升了(忽略了if块,直接提升至函数域顶部)。

问题二:函数名和变量名同名变量的问题?

函数是各种语言的一等公民,JS也不例外,因此如果函数和变量同名,变量提升后在作用域顶部访问同名变量会输出函数定义,也就是说:不论是先命名变量还是先命名函数,都会被覆盖为函数定义。

console.log(a)
var a
function a () {}

// ƒ a () {}
console.log(a)
function a () {}
var a

// ƒ a () {}

至于题主的问题,bs却没有被定义为函数,因此要参考我的答案:此时变量仅为VO而不是AO,具体规定要看ECMAScript规范。

问题三:let和const和ES6块级作用域?

let和const会形成阮老师提出的暂时性死区问题,实际表现为:在基本作用域(<script>和函数空间)下,初始化变量树时,如果访问到同名let或者const变量定义,会删除变量树上已经命名的变量并且阻止后续var和函数定义、未写的直接定义变量加入变量树(此时会报错已定义)。

var a
function b () {
  console.log(a)
  let a
}

b()

// Uncaught ReferenceError: a is not defined

而let和const加入变量树的行为,则是在执行到该语句的时候,并且与当前上下文的块结构强相关(新的一级执行上下文内定义该变量),因此后面该块内才能够访问该变量,而一旦执行位置离开当前块结构后(新的一级执行上下文),变量树上该变量也因此被删除,并不影响外部变量定义(父块的SC)。

var a = 1
{
  let a = 2
  console.log(a)
}
console.log(a)

// 2
// 1

问题四:function的规定和行为的差异?

ES5规定,不允许在块级区域定义函数;ES6规定,可以在块级区域定义函数,但是不存在变量提升,类似于let(可以理解为新增定义方式)。

但是!!重点!!

函数命名定义,浏览器从ES5开始就没有遵守规定,可以在块级内定义,因此ES6里面附录提到:为了向后兼容,不遵守就拉倒吧,就跟var一样吧,你开心就好。

因此,上面一大堆解释,全部是基于这个情况的。

更正

  • 重点参考 阮一峰老师ES6基础块级作用域一小节的说法,“ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用”:
function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    // 重复声明一次函数f
    function f() { console.log('I am inside!'); }
  }

  f();
}());

以上代码实际运行效果:

// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }
(function () {
  var f = undefined;
  if (false) {
    function f() { console.log('I am inside!'); }
  }

  f();
}());

故楼主的理解是对的,内部细节请参考@rhinel 同学的说法。

原答案所犯错误

在原答案中,我犯了以下错误:

  1. ES6新增的块级作用域只能通过let、const和花括号共同构建,没有直接通过花括号构成作用域的说法,原答案中例子是歪打正着
  2. ES5浏览器环境下输出2,是浏览器自行实现的结果,目的是兼容旧代码。根据规范,ES5的函数中不应使用函数声明,ES6的函数中允许使用函数声明,但其行为类似于let,且变量已经提升
  3. 无论是ES5还是ES6,都不应该在函数作用域中写函数声明,写成函数表达式更容易理解。

原答案

从每周精选里看到这个题,关于这个题我有不同想法。
如果在ES5中,楼主的题目应该打印出 2
因为函数声明会提升到函数级作用域的顶部,即

function bs(){
    console.log(1);
}
function fn(){
    function bs(){
        console.log(2)
    }
    bs();
    if(false){}
}
fn();   // 2

而本题之所以显示没有定义,是因为当前浏览器都已经支持ES6,if(false){}的花括号构成了块级作用域。以下修改(下例错误,关键是函数作用域内的函数声明在ES6环境下的表现是特例)能说明确实是块级作用域的问题:

// 单独加块级作用域{}
function bs(){
    console.log(1);
}
function fn(){
    bs();
    {    
        function bs(){
            console.log(2)
        } 
    }
    
}
fn(); // bs is not function

根据阮一峰ES6教程的说法“ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用”。而let定义存在暂时性死区:

var tmp = 123;

if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

“上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错”

因此,本题是ES6块级作用域与let暂时性死区的坑(应该是ES6环境下函数作用域中声明函数的坑)

变量的提升是有作用域的,如果在当前作用域下查不到该变量,才会去上一级作用域中查找,你给的例子中,fn函数的作用域内,function fn(){

bs();
if(false){
    function bs(){
        console.log(2)
    }
}

}相当于====》function fn(){
var bs; // 此时bs为underfined,当然不是一个function了

bs();
if(false){
   bs= function (){
        console.log(2)
    }
}

}

首先ES5虽然没有块级作用域但是有函数域,函数内部在此处没办法访问全局,

东哥
  • 1
新手上路,请多包涵

我的理解:
浏览器实现的es6对于函数在块级作用域中也是存在函数提升的,并且会在函数作用域或全局作用域中同时创建一个var声明的同名变量初始值为undefined,当执行到函数声明的代码时,会将当前变量的值同步到var声明的那个变量上。

(function () {
    debugger;
  if (true) {
    console.log(111)
    function f() { console.log('I am inside!'); }
  }
  console.log(1)
//   f();
}());

这个例子当在进入到块级作用域是在断点调试时当执行到console.log111时会看到local作用域存在f变量为undefined,且块block作用域也会存在f是个函数,只有当f声明的代码执行完会发现block中的f会同步给local中的f

宣传栏