资讯专栏INFORMATION COLUMN

ES5和ES6作用域详解

Dr_Noooo / 3177人阅读

摘要:允许在块级作用域内声明函数。上面代码中,存在全局变量,但是块级作用域内又声明了一个局部变量,导致后者绑定这个块级作用域,所以在声明变量前,对赋值会报错。

ES5的作用域

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

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

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

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

词法作用域和函数作用域

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

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

函数在调用结束时,该函数作用域会被销毁,里面的所有局部变量也会被销毁。

作用域链
console.log(a); // a is not defined

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

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

js中存在全局执行环境和由函数形成的局部执行环境这两种,统称为执行环境(这里可以理解成作用域);

执行环境都会对应一个变量对象,包含当前环境的变量和函数(函数中的参数也作为函数执行环境的变量,即函数所在作用域的局部变量,函数的活动对象包括this、arguments以及内部的变量和函数);

只有当函数执行时会形成作用域链,作用域链的前端始终是当前执行代码所在的执行环境对应的变量对象,往后是下一个(外部)变量对象,直到最外边的全局执行环境的变量对象(所谓的作用域链就是变量对象组成的一条线);

变量对象中变量的解析查找就是沿着作用域链一级一级查找;

如果执行环境中的变量没有用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();

上述答案是输出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

只提升函数对应变量,其他变量直接不提升,同时将变量的声明var去掉(在一般定义过程中不推荐使用同名变量)

函数和函数同名

都提升,但是后面的会覆盖前面的

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从来不会告诉你是否多次声明了同一个变量;遇到这种情况,它只会对后续的声明视而不见(不过,它会执行后续声明中的变量初始化)。

变量提升是分段的

段是指标签,代码执行时不分段的

 

    
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相同点

支持块级作用域;

变量不能提升;

存在暂时性死区

不可重复声明

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

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

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

相关文章

  • ES5ES6作用详解

    摘要:函数作用域虽然不存在真正意义上的块级作用域,但是存在函数作用域,为了解决上述伪块级作用域的问题,使用函数解决法如下结果为结果为注意以上生成函数作用域的写法存在两个问题,第一申明了全局的具名函数,污染了全局作用域。 ES5和ES6作用域 ES5的块级作用域 ES5的块级作用域是一个伪块级作用域,代码块:{},它的块里面和块外面都是共用一个作用域,即: Example: { var...

    sourcenode 评论0 收藏0
  • ES5ES6数组遍历方法详解

    摘要:和数组遍历方法详解在中常用的种数组遍历方法原始的循环语句数组对象内置方法数组对象内置方法数组对象内置方法数组对象内置方法数组对象内置方法数组对象内置方法数组对象内置方法数组对象内置方法循环语句中新增加了一种循环语句三种数组循环示例如下原始循 ES5和ES6数组遍历方法详解 在ES5中常用的10种数组遍历方法: 1、原始的for循环语句2、Array.prototype.forEach数...

    GitChat 评论0 收藏0
  • ES6 变量作用与提升:变量的生命周期详解

    摘要:不同的是函数体并不会再被提升至函数作用域头部,而仅会被提升到块级作用域头部避免全局变量在计算机编程中,全局变量指的是在所有作用域中都能访问的变量。 ES6 变量作用域与提升:变量的生命周期详解从属于笔者的现代 JavaScript 开发:语法基础与实践技巧系列文章。本文详细讨论了 JavaScript 中作用域、执行上下文、不同作用域下变量提升与函数提升的表现、顶层对象以及如何避免创建...

    lmxdawn 评论0 收藏0
  • JavaScript声明变量详解

    摘要:命令用于规定模块的对外接口,命令用于输入其他模块提供的功能所以在一定程度上来说,也具有声明变量的功能。当没有声明,直接给变量赋值时,会隐式地给变量声明,此时这个变量作为全局变量存在。 前言 如果文章中有出现纰漏、错误之处,还请看到的小伙伴多多指教,先行谢过 在ES5阶段,JavaScript 使用 var 和 function 来声明变量, ES6 中又添加了let、const、imp...

    paulquei 评论0 收藏0
  • [面试专题]ES6之箭头函数详解

    摘要:使用或调用由于已经在词法层面完成了绑定,通过或方法调用一个函数时,只是传入了参数而已,对并没有什么影响箭头函数不会在其内部暴露出参数等等,都不会指向箭头函数的,而是指向了箭头函数所在作用域的一个名为的值如果有的话,否则,就是。 ES6之箭头函数 标签(空格分隔): 未分类 返回值 单行函数体默认返回改行计算结果, 多行需要指定返回值 let c = (a,b)=>a+b; conso...

    Caicloud 评论0 收藏0

发表评论

0条评论

Dr_Noooo

|高级讲师

TA的文章

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