作用域的概念
了解什么是作用域之前我们先来了解下面的四个基本概念
vabiable:变量,是值得符号名称。变量的名称叫做标识符。如 let x = 1;其中x就是变量。
value:即数据。如字符串,数值,函数等。
name binding:变量和值所建立的联系。如 let x = 1。
scope:作用域,指代码中name binding的有效范围。
什么是作用域,在程序代码中,作用域就是变量和值所建立的联系的有效范围,通俗的来讲就是变量和函数的有效访问范围,作用域并不是JavaScript独有,任何程序语言中都会有作用域的概念,作用域分为静态作用域和动态作用域。静态作用域就是在“定义时”决定的,而动态作用域是在“调用时”决定的。而我们的JavaScript就是用的使用的静态作用域也叫词法作用域。
下面我们来看一段代码
function fn1() {
console.log(a);
}
function fn2() {
var a = 3;
fn1();
}
var a = 2;
bar(); //输出2
从上面这段代码可以看出结果会输出2,有的人可能会觉得奇怪为什么不是3,这是因为JavaScript采用的是静态作用域,当函数定义时就已经决定了它的作用域范围,fn1是定义在全局作用域中,所以fn1中访问的变量a是全局作用域中的a,也就是a = 2; 如果JavaScript采用动态作用域的话,由于动态作用域是在调用时才决定的,这时fn1的作用域就是在fn2中,所以就会访问到fn2中的a,也就会输出3。
声明提前
我们来看一段代码:
console.log(a); //输出undefined
var a = 10;
fn(); //输出"我被调用了"
function fn(){
console.log('我被调用了');
}
从这段代码可以看出JavaScript是存在声明提前的,在代码执行之前JavaScript引擎会进行一个代码解析。用var声明的变量和函数会被提升至作用域的顶部,但赋值不会提前所以这里a会输出undefined,方法fn也能够正常的调用。这样讲会比较的抽象,我们来看下面一段代码,它等价于上面的代码。
function fn(){
console.log('我被调用了');
}
var a;
console.log(a); //输出undefined
a = 10;
fn(); //输出"我被调用了"
注意,在提升中函数提升的优先级是大于变量的。下面我们来看一个例子:
console.log(fn)
var fn = 5;
console.log(fn)
function fn(){
console.log('我被调用了');
}
function fn(){
console.log('我被调用了');
}
var fn
console.log(fn)
fn = 5;
console.log(fn)
如果我们把代码改一下,把函数fn改成函数表达式会是怎么样呢。
console.log(fn)
var fn = 5;
console.log(fn)
var fn = function (){
console.log('我被调用了');
}
console.log(fn)
这个时候的函数fn就被当成一个普通的变量赋值。所以会输出undefined,5 ƒ () { console.log('我被调用了'); }。等价于下面的代码
var fn;
console.log(fn)
fn = 5;
console.log(fn)
fn = function (){
console.log('我被调用了');
}
console.log(fn)
块级作用域
块级作用域是指变量在指定块的作用域之外部无法被访问,它位于一对花括号中,在ES6中let 和const 关键来声明变量,用这2个关键字可用创建块作用域变量。下面变量x和常量y是无法在当前块作用域外部访问的。
if(true){
let x = 5;
const y = 10;
}
console.log(x,y);
在ES6以前,我们写一个for循环会用var 定义一个变量i,通常是因为只想在 for 循环内部的上下文中使 用 i,而忽略了 i 会被绑定在外部作用域(函数或全局)中的事实。此时在外部访问i是可以访问到的。
for(var i = 0; i < 5; i++){
//....
}
console.log(i); // 输出5
而当我们使用let关键字来定义i时,会报错,这就是块作用域的用处。变量的声明应该距离使用的地方越近越好。
for(let i = 0; i < 5; i++){
//....
}
console.log(i); // x is not defined
let声明的变量有“暂时性死区”的特性(也就是说声明前不可用)。下面这段代码i在被声明之前被使用了,会报错。
function fn(){
//..../
console.log(i); // x is not defined
let i =10;
}
fn();
var定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问。
let定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问。
const用来定义变量,使用时必须初始化(即必须赋值),只能在块作用域里访问,如果改变赋值为基本数据类型,不能修改,引用类型可以修改其内容。
在ES3 规范中规定 try/catch 的 catch 分句会创建一个块作 用域,其中声明的变量仅在 catch 内部有效。例如
try {
undefined(); // 执行一个非法操作来强制制造一个异常
} catch (err) {
console.log( err ); // 能够正常执行!
}
console.log( err ); // ReferenceError: err not found
作用域链
什么作用域链叫作用域链,当访问一个变量时,会首先从当前作用域中去寻找这个变量是否在,如果没有找到,则会向上一级的作用域中区寻找,如果还没有找到则继续向上上级作用域去寻找,一直找到顶端作用域,如果找到了就直接使用,如果没有找到则会报错。这样沿着作用域向上一级级寻找方式就组成了一条链条式的结构,所以我们就叫它作用域链,是不是很形象呢。作用域链的长度取决于作用域嵌套的层数,层数越多则作用域链越长。下面我们来看一段代码:
let a = 5;
function fn(){
let b = 10;
function bar(){
console.log(a,b);
}
bar();
}
fn();
这段代码首先JavaScript引擎会在当前作用域bar中区寻找a和b,没有找到。则向上层作用域fn中继续查找,这时找到了变量b,没有找到变量a,所以继续向上层的全局作用域中查找变量a,这时候找到了变量a,所以可以使用,最后输出5,10;
总结
本篇文章从四个方面讲解了作用域,本人菜鸟一个,只是很粗浅的讲解了js作用域的知识,如果错误之处还请各位大佬指出。另外强烈推荐一下《你不知道的JavaScript》这本书,里面有详细的记载了js作用域的知识。