js里的作用域问题

149 阅读7分钟

引言

大家好,今天来给大家讲一下js中常用的作用域问题,相信大家对这个问题都不陌生,但是有些细节,可能大家并不熟悉,本文将会在这为大家讲述。

大家能在这里学到:

  1. js的解析过程
  2. 变量提升优先级等
  3. 常见的作用域,全局作用域,函数作用域,块级作用域,局部作用域({}包裹的)
  4. let和const的区别以及为什么const能进行修改
  5. 函数参数作用域
  6. 立即执行函数的作用域

js的解析过程

js一般代码的执行顺序是自上而下,但是关键词会被提升到当前作用域的顶层,因为js会转化为Ast树,但是只是定义了这个变量,并没有给这个变量进行赋值,如

console.log(a);//undefined
var a=12
console.log(a);//12
var a =14
console.log(a);//14
//等价于
//var a
//console.log(a)
//a=12
//console.log(a)
//a=14
//console.log(a)

但是函数是有点区别的,函数分为函数表达式和函数声明式,当函数遇到function是会把他当关键词提升到当前作用域顶层。

function a(){ //函数声明式
    console.log('123');
}

let b= function(){ //函数表达式
    console.log('123');
}

当函数名和变量名(或者叫标识符,不是科班,错了请见谅)相同时,这个时候会比较他们的优先级,函数的的优先级是优于变量的,具体代码如下:

console.log(a); //  [Function: a]
var a =3

function a(){
    console.log('123');
}

但是使用函数表达式并不会这样,函数表达式没有变量提升

console.log(a); // undefined
var a =3
console.log(a) // 3
 var a=function a(){
    console.log('123');
}
console.log(a) // [Function: a]

// 实际执行顺序如下
// var a
// console.log(a); 
//  a =3
// console.log(a)
//  a=function a(){
//     console.log('123');
// }
// console.log(a)

js的作用域

初略分为全局作用域,函数作用域,块级作用域,每次变量查找的时候,都会先在当前作用域内进行查找,原型的查找也是这样。

js是使用栈来存储上下文的,分为ao,和vo两个属性来记录,ao是当前活跃的上下文,vo是父级上下文(应该不是这样的,但是这样的效果),每次进去一个新的上下文就向栈内push进去,离开就取出。

image.png

全局作用域

全局作用域就是最外面的默认是window,但是node好像是不一样的,后面新出了个global(这个好像是this的)来统一 var是最一开始出现的,他和别的不同,他定义的变量会定义到他当前作用域里面,但是let和const不同,他们会生成自己的块级作用域。还是上代码吧

let a =123

var a=321

const a= 432 
//上面代码写的时候会提示报错,因为同一作用域内let和const的变量不能重复定义,但是上面的var不会出现这个问题

ps js里如果不用标识符,直接定义一个变量,这个变量会直接定义到全局变量里,但是如果使用严格模式的话,就会报错。

function a(){
    b=13
}
a()
console.log(b);//13

//严格模式
function a(){
    'use strict'
    b=13
}
a()
console.log(b);//ReferenceError: b is not defined

块级作用域

let和canst都会生成块级作用域

块级作用域有其实也是有提升的,但是默认隐藏了(红宝书还是你不知道的JavaScript里说的),

因为let的作用域是块,所以不能检查前面是否使用let声明过同名变量,同时也就不可能在没有声明的情况下声明他,红宝书里说的

结论就是没到let定义的变量时,提前使用该变量会导致报错,无法引用该变量,也被称为暂时性死区。

let和const的区别

let定义的变量可以进行更改但是const的不允许更改,但是这是const定义的变量是保存基本类型,如果是复杂的数据类型对象,是可以进行更改的,这里要提一下js的存续数据方式了。

js是使用栈和堆来进行数据存储的,java好像也是这样的,普通的数据类型是存储在栈里的,复杂的数据类型是存储在堆里,栈里只是用一个变量来记录这个堆的地址,我们平时修改对象其实就是修改栈里这个变量存储的堆地址上的内容,但是地址是没有变的,这就是const定义一个对象,但是对象可以被修改的原因。

image.png ps 当前作用域内只要使用了let或者const定义了变量,那么在执行到定义位置之前,都是报错,即使前面没用标识符 直接定义为全局变量也会报错(会报已声明),形参也是。

//第一个例子
let age =18
var name1='jack'
function a (){
    console.log('1:',age); //Cannot access 'age' before initialization 报错后面都不执行
    //也有人说可能是下面age没执行到,要是执行了,绝对可以,输出
    console.log('1:',name1);
    age = 18
    console.log('2:',age);
    console.log('2:',name1);
    let age =22
    console.log('3:',age);
    console.log('3:',name1);
}
a()
console.log('4:',age);
console.log('4:',name1);

//********************************************************************************
//第二个例子,还是一样的结果,这就是很多面试题考的,let会导致暂时性死区
let age =18
var name1='jack'
function a (){
    // console.log('1:',age);
    // console.log('1:',name1);
    age = 18 //Cannot access 'age' before initialization 报错
    console.log('2:',age);
    console.log('2:',name1);
    let age =22
    console.log('3:',age);
    console.log('3:',name1);
}
a()
console.log('4:',age);
console.log('4:',name1);

//********************************************************************************
//第三个例子
let age =18
var name1='jack'
function a (){
    // console.log('1:',age); //Cannot access 'age' before initialization 报错
    // console.log('1:',name1);
    // age = 18
    // console.log('2:',age);
    // console.log('2:',name1);
    let age =22
    name1='ccc'
    console.log('3:',age);
    console.log('3:',name1);
}
a()
console.log('4:',age);
console.log('4:',name1);

// 输出结果
// 3: 22 因为作用域查找是现在当前查找再逐级向上,所以是22
// 3: ccc  这个也是作用域链查找,不是直接定义全局变量,下面个例子能证明
// 4: 18 因为作用域隔离,所以外面的没被修改
// 4: ccc 引用的是这个外部的,被修改了

//********************************************************************************
//例子4
let age = 18;
var name1 = "jack";
function a() {

  let age = 22;
  let name1 = "ccc";
  console.log("3:", age);
  function b() {
    name1 = "ddd";
    console.log("5:", name1);
  }
  b();
  console.log("3:", name1);
}
a();
console.log("4:", age);
console.log("4:", name1);
// 结果
// 3: 22
// 5: ddd
// 3: ddd
// 4: 18
// 4: jack

//例子5
function a(num1,num2){
    console.log('num1',num1); //num1 1
}
a(1)//num1 1

//例子6
function a(num1,num2){
    num1 =3
    console.log('num1',num1); //Identifier 'num1' has already been declared
    let num1=2
}
a(1)

参数作用域

这个作用域可能很少有人听说,他是定义在函数的参数里的,平常我们定义两个形参a和b,a是个普通变量,b是个函数,引用a,这时候就会形成一个独特的作用域,参数作用域,该作用域的vo是函数所处的上下文,并且形参是一个全新的变量,不再和传入的实参是同一个变量。

function a(num1,num2=()=>{num1=1; console.log(num1)}){
   console.log('num1:',num1);  //num1: undefined
    num2()  //1
    console.log('num1:',num1);  //num1: 1
}
a()

image.png

立即执行函数IIFE

现在很多人估计只知道他的作用立即执行,但是对他的细节并不是很了解,这个函数的作用是生成一个独有的作用域,来避免数据受到污染的,es6之前,是只有全局作用域和函数作用域的,是没有块级作用域的,立即执行函数就是用来处理这种情况的.

解析方法和结构

上文也说了,浏览器解析的时候遇到function时,会把他进行提升,但是立即执行函数我们是要他进行立即执行,所以我们用()将他进行包裹,这样就避免了提升,但是这时他是没有执行的 我们平常是先进行了函数声明最后再函数名()进行调用,其实立即执行函数也是如此,可以在()后面加(),也可以在函数结尾加()来进行调用,这是js进行了一个设定

注意事项

我们可以在该函数体内调用该函数,只需要该函数名()即可,但是这个函数名是已经给定义死了的,不允许修改,如果我们进行修改不会进行报错,但是该代码无效。

ps 使用立即执行函数前面最好用;进行隔绝,因为有可能js会把IIFE当成a(IIFE)这种来进行识别

let a= b //但是加了;会断开
(function a(){})()
//浏览器等价于
// let a= b(function a(){})()

结语

这都是我们平常写代码时经常遇到的,但是我们平时可能不会注意到这种细节,这种就可能导致我们写的代码出现问题,还有with和eval也可以加作用域。

到了这里,今天的学习就到此为止吧。