我眼中的js编程(2)--详解作用域内变量和函数的声明与访问

我眼中的js编程(1)主要介绍了js是用来做什么的,这一篇开始及以后总结js具体该怎么用。本篇总结了作用域内变量和函数的声明与访问。先看一段有意思的代码。

var a = a;
console.log(a) // undefined

let b = b;
console.log(b) // ReferenceError

let c;
c = c;
console.log(c) // undefined

很有意思的结果。为什么是这样呢?这个问题放一边,我先扯点没用的,等读到文章最后,如果你理解了,就一定会知道为什么。

(ps:先扯会儿淡)所有的编程语言都有相通之处,也有其各自擅长的地方。js和其他语言一样,数组、函数、对象等数据类型以及各个数据类型处理数据的api、运算符、变量的作用域、对象的创建和继承,这些概念,虽然各种语言的语法规则不一样,但是本质上是一样的。一通百通,没有太多新鲜之处。

js的独特之处在于能够处理网页的交互效果。这事儿只有js能干,因为浏览器只有js引擎,没有php引擎、java引擎,为什么是这样?这和js与浏览器的历史有关,阮一峰老师有一篇文章Javascript诞生记很好的做了诠释。

作用域

作用域有什么用?某个功能的代码,把其中没有必要暴露的函数和变量封装起来,实现最小暴露。

(ps:继续扯淡)软件设计中有最小暴露原则,就是用来实现某功能的代码,应该最大限度的暴露最少的东西。不止是软件,生活中的事物也遵循着这个原则,比如数据线,只是暴露了一个和手机的接口,其余的线路都在包装线内部隐藏,比如智能手机,暴露在外面的就是个外壳和屏幕,复杂的线路和芯片被隐藏在手机内部,而拆解开手机内部的具体零件,每个零件同样也是遵循着最小暴露原则,甚至社会组织结构比如饭店,大厅的餐桌、服务员对外暴露,而后厨的炊具、厨师隐藏在内部,而且仔细观察生活中的各类事物,无不遵循这这样的原则,而且是递归最小暴露,即拆解开来的零件同样也遵守,零件的零件仍然遵守。脑洞大开扯远了。。。下面说一下作用域怎么使用?聊聊作用域的相关规则(先聊有啥用,再聊怎么用,要知其然和所以然)。

变量的作用域在写代码的时候就确定了。es6之前js只有全局作用域和函数作用域(try-catch语句的作用域、eval()方法的作用域等暂不考虑),在es6中有了块作用域、新的全局let作用域、for循环作用域、模块作用域(参考自深入浅出es6)。js中变量的作用域是整个前后封闭的函数代码块,而不是开始于变量声明之处(有些编程语言的作用域是这样的)。嵌套作用域是编程语言的核心理念之一,js中常见的作用域()有:

  • 函数作用域
    var声明的变量所在的函数的整个代码块。
  • 块作用域
    let(const)声明的变量(常量)所在的外层块{ }
if(true){
  let a = 5
}
console.log(a) // ReferenceError
  • 新的全局let作用域
    let声明的全局变量不是全局对象的属性,let声明的全局变量存在于一个不可见的块作用域中,理论上是页面中包含所有js代码的不可见的外层块。
let a = 1
console.log(window.a) // undefined
  • for作用域
    for循环中()中变量是let声明的时候,比如for(let i = 0;i<5;i++){...},每次迭代都为i绑定新的块作用域,这个块就是for(){ }的{ }
for(let i = 0;i<5;i++){
  setTimeout(function(){
    console.log(i) // 每个1s输出一次,分别输出0 1 2 3 4
  },1000 * i)
}
  • 模块作用域

常量和变量

js中的变量和常量是需要用关键字声明的(php不用声明)
关键字var声明变量:var age = 18
关键字let声明变量:let name = 'yanhaoqi'
关键字const声明常量:const STUDENT_NUM = 30
常量和变量有全局局部之分,下面以变量为例说明。

  • 全局变量
    全局变量定义在全局对象中,可在任何作用域访问到。
    ECMAScript本身具有全局对象,但全局对象不是任何其他对象的属性,所以它没有名字。浏览器环境的全局对象是window,表示允许js代码的浏览器窗口,浏览器窗口就是浏览器端js的最大操作范围(权限)。node环境下的全局对象是globle
  • 局部变量
    局部变量声明在局部作用域,只在局部作用域内可见。

下面主要讲 作用域内变量的访问规则
先看两段代码:

console.log(name) // undefined
var name
console.log(name) // undefined
name = 'yanhaoqi'
console.log(name) // yanhaoqi
console.log(age) // ReferenceError
let age
console.log(age) // undefined
age = 18
console.log(age) // 18

为什么会有这样的区别呢?你可能会说,let声明的变量没有变量提升,其实这样说是不完全准确的,在这里我深入讨论下一些细节。

js引擎在编译和解释代码的时候,声明的变量有三个阶段:
声明阶段(Declaration Phase) :在当前作用域中注册一个变量(作用域在编译和解释之前已经确定)
初始化阶段(Initialization Phase):在作用域中为变量绑定内存,变量初始化为undefined。
赋值阶段(Assignment Phase):为初始化的变量分配一个具体的值。

var声明的变量

上面第一段代码,js引擎编译和解释过程如下:

  • 第一步
    在执行任何语句之前,先找到这段代码中所有的声明var name进行处理(引擎在编译时候的任务之一)
    name变量在任何代码执行前先在作用域顶部通过了 声明阶段 ,在作用域注册了变量name
    然后紧跟着来到 初始化阶段 ,name初始化为undefined,两个阶段之间没有任何间隙
    这个过程叫变量提升
  • 第二步
    开始执行第一句代码console.log(name),此时结果是undefined
    然后开始执行第一句代码console.log(name) 结果是undefined
  • 第三步
    执行 var name,没什么实际意义,因为一开始引擎就找到了var声明进行了处理,继续执行后面console.log(name)结果仍然是undefined,这里就是为了对比下面let代码的结果。
  • 第四部
    执行后面的console.log(name)结果是yanhaoqi
我眼中的js编程(2)--详解作用域内变量和函数的声明与访问_第1张图片
屏幕快照 2017-09-13 下午4.39.20.png
let声明的变量

上面第二段代码,js引擎编译和解释过程如下:

  • 第一步
    在执行任何语句之前,先找到这段代码中所有的声明let age进行处理,age变量在任何代码执行前先在作用域顶部通过了 声明阶段 ,在作用域中注册了变量name。不会紧接着进行初始化阶段。这算不算let声明的变量的提升我查到的资料上说法不一,但本质的过程就是这样的。
  • 第二部
    开始执行第一句代码console.log(age),因为age还没有经历初始化阶段,没有被分配内存和初始化为undefined,所以会报错ReferenceError
  • 第三步
    执行代码let age此时age变量才会进行初始化。接着执行console.log(age)结果是undefined
  • 第四部
    执行代码age = 18。此时完成 赋值阶段

变量提升的问题,弄清楚变量的声明和访问的过程就ok了,至于有人说let声明的变量没有变量提升,有人说let声明的变量是不完全提升,说法不同而已,管他呢,本质就是这样的。

我眼中的js编程(2)--详解作用域内变量和函数的声明与访问_第2张图片
屏幕快照 2017-09-13 下午5.54.49.png

var声明的变量在一开始就完成了 声明阶段初始化阶段,两个阶段是连在一起的,而let声明的变量要执行到let时候才会完成 初始化阶段let声明的变量完成了声明阶段还没有到达初始化阶段的时候如果访问该变量就会报错ReferenceError,我们称变量此时处在临时死区(Temporal Dead Zone,简称TDZ)

函数声明的提升

既然上面详细解释了变量的声明和访问的过程,顺便接着说一下函数声明的提升。首先要搞清楚函数声明和函数表达式的区别,如果关键字function是函数定义的第一个词,那这就是一个函数声明,否则就是一个函数表达式。

function foo(){
  console.log(123)
}
函数声明
var foo = function(){
  console.log(123)
} 
函数表达式
(function foo(){
  console.log(123)
})
函数表达式

明确了什么是函数声明后,下面我们讨论下函数声明的访问。

[2,3,4,5].reduce(multiplier); // 120
function multiplier(a,b){
  return a * b;
}

在定义multiplier函数之前就把它作为参数传入了reduce()函数,为什么函数在定义之前就可以使用?我们看下js引擎执行这段代码的具体编译和解释的过程。

  • 第一步
    js引擎在执行任何代码之前先找到函数声明,并在对应的作用域的顶部完成 声明阶段初始化阶段赋值阶段
  • 第二步
    开始执行第一句代码[2,3,4,5].reduce(multiplier);,函数multiplier作为reduce()的参数。
我眼中的js编程(2)--详解作用域内变量和函数的声明与访问_第3张图片
屏幕快照 2017-09-13 下午6.34.14.png

最后,关于js语言的设计中的变量提升和函数提升的规则,为什么有变量提升的设计,这要问js作者Brendan Eich,网上这方面信息较少,我在这里给一篇我觉得解释的不错的博客点击查看。

总结var let声明的变量和函数声明的访问的区别就是,var声明的变量,声明阶段、初始化阶段2个阶段是耦合的,let声明的变量,声明阶段和初始化阶段是解耦的,而函数声明的声明阶段、初始化阶段、赋值阶段三个阶段都是耦合的。

点击查看上一篇我眼中的js编程(1)
点击查看下一篇我眼中的js编程(3)
我眼中的js编程系列是我个人的学习总结,如有错误,烦请包涵、不吝赐教,O(∩_∩)O谢谢

你可能感兴趣的:(我眼中的js编程(2)--详解作用域内变量和函数的声明与访问)