6

ES5的作用域

变量起作用的范围,js中能创建作用域的只能是函数

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

var的作用域就是所在的函数体

let的作用域就是所在的代码块

词法作用域和函数作用域

当代码写好的时候,能够根据代码的结构确定变量的作用域,这种情况下的作用域就是词法作用域。js就是此法作用域,不是动态作用域。

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

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

作用域链

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


Embrace
248 声望5 粉丝