ES5和ES6做用域详解

ES5的做用域

变量起做用的范围,js中能建立做用域的只能是函数javascript

{
  let a = 1;
  var b = 2;
}
console.log(a); // a is not defined
console.log(b); // 2

var的做用域就是所在的函数体html

let的做用域就是所在的代码块前端

词法做用域和函数做用域

当代码写好的时候,可以根据代码的结构肯定变量的做用域,这种状况下的做用域就是词法做用域。js就是此法做用域,不是动态做用域。java

在某个函数中使用var声明变量,那个变量就将被视做一个局部变量,只存在于函数中.es6

函数在调用结束时,该函数<wiz_tmp_highlight_tag class="cm-searching" style="margin-top: 0px; background: yellow;">做用域</wiz_tmp_highlight_tag>会被销毁,里面的全部局部变量也会被销毁。web

做用域链

console.log(a); // a is not defined

function test() {
   a = 1;
}
test();

(根据javascript高级程序设计第四章)解析上面的代码面试

  1. js中存在全局执行环境和由函数造成的局部执行环境这两种,统称为执行环境(这里能够理解成做用域);
  2. 执行环境都会对应一个变量对象,包含当前环境的变量和函数(函数中的参数也做为函数执行环境的变量,即函数所在做用域的局部变量,函数的活动对象包括this、arguments以及内部的变量和函数);
  3. 只有当函数执行时会造成做用域链,做用域链的前端始终是当前执行代码所在的执行环境对应的变量对象,日后是下一个(外部)变量对象,直到最外边的全局执行环境的变量对象(所谓的做用域链就是变量对象组成的一条线);
  4. 变量对象中变量的解析查找就是沿着做用域链一级一级查找;
  5. 若是执行环境中的变量没有用var声明,那么在函数执行时(这样才会造成做用域链)会沿着做用域链一级一级查找变量对象,若是没有找到则会在全局变量对象中声明该变量并初始化。

综上,上面的代码能够改写成下面这样数组

function test() {
   a = 1;
}
test();
console.log(a); // 1

闭包

闭包是指有权访问另外一个函数做用域中的变量的函数浏览器

首先区分一点就是函数内部定义的函数,其做用域链会包括外部函数。请看下面两个例子对比闭包

// 案例一

function foo(){
  var num = '123';
  function bar(){
    console.log(num); // '123'
  }
  bar();
}
foo();

案例二
function bar(){
  console.log(num);// num is not defined
}
function foo(){
  bar();
}
foo();
<script>
    var arr = [];
    for(var i = 0; i<10; i++) {
        arr.push(function(){
        console.log(i);
    })
}
    arr[0]();
    arr[1]();

</script>

上述答案是输出10

分析: 数组中每一个函数若是执行时其做用域链都会保存全局执行环境对应的变量对象,因此函数执行时函数内部的i变量会沿着做用域链找到全局执行环境中的变量,此时全局执行环境中的变量i为10,因此都会输出10.

若是想输出1,2,3...,
第一种方法能够把for循环中的var变为let,变量ilet声明的,当前的i只在本轮循环有效,因此每一次循环的i其实都是一个新的变量,因此最后输出的是6。你可能会问,若是每一轮循环的变量i都是从新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是由于 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。摘自《ECMAScript 6 入门》
第二种方法就象下面案例一下再写一个for循环;
第三个方法就如同案例三在函数内部再定义一个函数,并当即执行外部函数,使函数的活动对象中变量i的值每次都不一样,从而保证内部函数在执行时其做用域链会包括外部函数的变量对象。
(通常来讲函数执行完毕该函数的做用域和活动变量会被销毁,可是由于函数里面定义的函数它的做用域链始终会包括外部函数的活动对象,因此外面的函数即便当即执行了,可是活动对象还在内存中,没有被销毁)

下面的案例再次巩固知识点,第二个案例再执行时,全局执行环境的变量对象i又从新被动态赋值,for循环中函数当即执行,由于函数的参数是按值传递的,因此每一个函数获得的是不一样的i值。

// 案例一
var arr = [ { name: '张三1'},
            { name: '张三2' },
            { name: '张三3' },
            { name: '张三4' } ];
for ( var i = 0; i < arr.length; i++) {
     arr[ i ].sayHello = function () {
         console.log(i);
      };
}
arr[0].sayHello();
arr[1].sayHello();
// 案例二
var arr = [ { name: '张三1'},
            { name: '张三2' },
            { name: '张三3' },
            { name: '张三4' } ];
for ( var i = 0; i < arr.length; i++) {
    arr[ i ].sayHello = function () {
        console.log(i);
    };
}
for ( var i = 0; i < arr.length; i++ ) {
    arr[ i ].sayHello();
}
// 案例三
var arr = [ { name: '张三1'},
            { name: '张三2' },
            { name: '张三3' },
            { name: '张三4' } ];
for ( var i = 0; i < arr.length; i++) {
    arr[ i ].sayHello = (function (i) {
       return function(){
          console.log(i);
       }
    })(i);
}

变量提高

分为预解析阶段和执行阶段

在预解析阶段,会将全部的变量声明(只提高声明不提高赋值)以及函数声明(指整个函数),提高到其所在的做用域的最顶上,通常会先提高函数声明再提高变量声明。

注意区分函数声明和函数表达式声明的区别
在变量提高条件下函数表达式和通常变量的声明的规则是同样的。下面的条件式声明章节还会用案例做对比

函数声明变量提高

  • 函数声明会被提高(是指整个函数都会被提高)
// 函数声明

fn();

 function fn() {
   console.log('hello world');
}
  • 函数表达式不会被提高(是指只会提高声明该函数的变量)
// 函数表达式
fn();

var fn = function() {
  console.log('nihao');
}

如下是变量提高中的特别状况

在变量提高状况下,变量通常被分红两种,即通常变量和函数名变量

变量和函数同名

console.log(typeof f);  // function

var f;

console.log(typeof f); // undefined

function f(){};

console.log(typeof f); // undefined
console.log(typeof a); // function

function a() { }

console.log(typeof a); // function

var a = '';

console.log(typeof a);  // string

<font color="red">只提高函数对应变量,其余变量直接不提高,同时将变量的声明var去掉(在通常定义过程当中不推荐使用同名变量)</font>

函数和函数同名

都提高,可是后面的会覆盖前面的

func(); // second func
function func(){
    console.log("first func");
}

function func(){
    console.log("second func");
}

变量和变量同名

// 通常的变量同名对于变量的提高没有影响,由于提高的只是变量的声明,不会提高变量的赋值

console.log(typeof a); // undefined

var a = 'abc';

console.log(a); // 'abc'

var a = 1;

console.log(a); // 1

下面有两个小栗子

var a = 1;
var a = 2;
console.log(a); // 2
var a = 1;
var a;
console.log(a); //1

刚刚去查了资料,参考《JavaScript高级程序设计》第7.3章节,原话以下

JavaScript历来不会告诉你是否屡次声明了同一个变量;遇到这种状况,它只会对后续的声明视而不见(不过,它会执行后续声明中的变量初始化)。

变量提高是分段的

段是指<script></script>标签,代码执行时不分段的

<script>

        var num = 10;

        func(); // 第二个func
        
        console.log(str); // 报错

        function func(){
            console.log("第一个func");
        }

        function func(){
            console.log("第二个func");
        }

    </script>

    <script>
        var str = 'abc';
        
        console.log(num); // 10
        
        func(); // 第三个func
        function func(){
            console.log("第三个func");
        }
    </script>

ES5块级做用域中的函数声明

ES5 规定,函数只能在顶层做用域和函数做用域之中声明,不能在块级做用域声明。若是确实须要,也应该写成函数表达式,而不是函数声明语句。

test();  //报错

if(true){
    function test(){
    console.log("我是在if语句中声明的函数");
  }
}
// 各个浏览器执行结果不一样,不建议这么写
if(flag){
   functiont test(){
      console.log("flag为true时执行");
  }
}else{
    function test(){
      console.log("flag为false时执行");
    }
}

上面两种状况在ES5中都是非法的,能够将上面的demo改写成下面这样

// 下面会根据flag状态决定执行哪段代码
if(flag){
    test = functiont(){
      console.log("flag为true时执行");
  }
}else{
    test = function(){
      console.log("flag为false时执行");
    }
}

条件式变量声明能够被提高。

console.log( num ); // undefined
if ( false ) {
   var num = 123;
}
console.log( num ); // undefined

ES6的做用域

块级做用域

块级做用域就是包含在{}里面的

function f1() {
  let n = 5;
  if (true) {
    let n = 10;
  }
  console.log(n); // 5
}

ES6块级做用域中函数声明

ES6 引入了块级做用域,明确容许在块级做用域之中声明函数。ES6 规定,块级做用域之中,函数声明语句的行为相似于let,在块级做用域以外不可引用。

原来,若是改变了块级做用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻所以产生的不兼容问题,ES6在附录B里面规定,浏览器的实现能够不遵照上面的规定,有本身的行为方式

  • 容许在块级做用域内声明函数。
  • 函数声明相似于var,即会提高到全局做用域或函数做用域的头部。
  • 同时,函数声明还会提高到所在的块级做用域的头部。

下面的例子可以很好的区分解释ES5和ES6两个环境下处理块级做用域中函数声明的区别

function f() { console.log('I am outside!'); }

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

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

  f();
}());
// Uncaught TypeError: f is not a function

let和const都存在暂时性死区

只要块级做用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,再也不受外部的影响。

var tmp = 123;

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

上面代码中,存在全局变量tmp,可是块级做用域内let又声明了一个局部变量tmp,致使后者绑定这个块级做用域,因此在let声明变量前,对tmp赋值会报错。

或者

{
  var a = 1;
  let a = 1;
}
// 报错 Uncaught SyntaxError: Identifier 'a' has already been declared

“暂时性死区”是指在使用let命令声明变量以前,该变量都是不可用的。

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

let&const相同点

  1. 支持块级做用域;
  2. 变量不能提高;
  3. 存在暂时性死区
  4. 不可重复声明

let&const区别

  • let声明变量,const声明常量
  • const声明时必须赋值
const foo;
// SyntaxError: Missing initializer in const declaration
  • 声明基本数据类型必须是写死的常量
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.
  • const声明引用数据类型必须是变量的内存地址不变

ES5 只有两种声明变量的方法:var命令和function命令。ES6 除了添加letconst命令,还有import命令和class命令。因此,ES6 一共有6种声明变量的方法。

ES6中顶层对象

  • 现状: ES5顶层对象很混乱
    浏览器里面,顶层对象是window,但 Node 和 Web Worker 没有window

浏览器和 Web Worker 里面,self也指向顶层对象,可是Node没有self
Node 里面,顶层对象是global,但其余环境都不支持。(详见http://es6.ruanyifeng.com/#do...

  • ES6的变更
    ES6 为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另外一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。

ES6中也存在做用域链

let test = 'out';
function  f(){
  test = 'in';
  console.log(window.test);// undefined
}
f();
console.log(test);// in

上下两个demo的区别就是test有没有用let声明,当使用let声明时,浏览器会认为当前的环境是ES6环境,因此声明的变量不会复制给window;相反若是没用let声明test,浏览器就会默认当前环境是ES5。

test = 'out';
function  f(){
  test = 'in';
  onsole.log(window.test); // in
}
f();
console.log(test);// in

经典面试题案例

案例一

var num = 123;
   function f1() {
       console.log( num );
    }

   function f2() {
        num = 456;
        f1();
    }

f2();

上述执行结果为456

相关文章
相关标签/搜索