前端修行,从var、let和const关键字开始+面试题--JS基础篇(三)

203 阅读10分钟

写在前面

如何打好JavaScript基础?深入剖析V8的代码执行机制--JS基础篇(一)

有关闭包的那些事,一文搞定!--JS基础篇(二)

在学习作用域和闭包时,我从JS的编译角度体会了代码在V8引擎中执行的艺术。我认为这是很有必要的,作用域和闭包的知识可以让我从整体上把握读懂一份代码的条件,从原本的只会看,到现在学会自己梳理代码的编译和执行过程,确实升华了很多。

👀学习了JS一段时间,我感觉它好像一位蒙着面纱的姑娘,每个角度看去,都有让我不得其解得的地方。不过,越是这样我越好奇。这次,我将从随处可见的变量开始,揭开JS的面纱。

这次从变量讲起,涉及到前面几篇的知识也会说明。

变量的引入

变量就是JS里面的一个容器,用于存放各种类型的数据。

数据类型(7种)
  • Number类型:表示整数和浮点数,其中浮点数最大值:Number.MAX_VALUE,最小值:Number.MIN_VALUE。
var num = 35;
var num1 = 3.33;

Number类型上其他相关的静态数据属性:

Number.MAX_SAFE_INTEGER 最大的安全整数(2^53 - 1)

Number.MIN_SAFE_INTEGER 最小的安全整数(-2^53 - 1)

“安全”指的是能够精确表示整数并正确比较它们

  • String类型:表示字符序列,通过字符串字面量方式创建原始值或String() 方式创建字符串对象。
var str = 'I love JavaScript!';
var str1 = new String('Me too.');
  • BigInt类型:一个数字的原始值,表示大于2^53-1的整数,可以通过在一个整数字面量后面加n的方式创建或调用函数BigInt() 创建。
var bigNum = 8888888888888888888n;
var bigNum1 = BigInt(99999999999999999999);
  • Boolean类型:表示一个逻辑实体,值为truefalse
var bool = true;
var bool1 = false;
  • undefined类型:表示值的缺失,只定义了,但没有赋值,唯一值为undefined,一个没有被赋值的变量类型为undefined。
var un = undefined;
  • Null类型:唯一值为null,表示变量未指向任何对象。
var nu = null;
  • Symbol类型(原子类型):唯一且不可变的原始值,为对象创建一个唯一的属性键,保证不会与其他键冲突。
const symbol = Symbol(1);
const symbol1 = Symbol(2);

console.log(symbol === 1);// false

console.log(symbol === symbol1);// false

console.log(Symbol(1) === Symbol(1));//false

console.log(Symbol(1) == Symbol(1));//false

变量的引入,赋予了JS这门编程语言生命。变量是代码的运行基础,因为没有变量存储数据,再华丽的代码也只是无用的工具。

那么,如何定义变量(选择哪个关键字)在哪里定义变量就应当十分考究了!

定义变量

JS目前提供了三个定义变量的关键词:varlet和*const。从变量状态来看,const定义的变量状态为不可修改(在大部分情况下),也叫做常量,变量名用大写表示。我们先单独看看。

var关键字

var是三者当中官方最早给出的一个,用于在全局作用域函数作用域内声明变量的关键字,其声明的变量默认值为undefined。

  • var变量的声明提升

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

    等价于:

    //注意这样写是错误的,这里只是为了演示
    var a;
    console.log(a);
    a = 2;
    

    此处打印的结果就是undefined,因为var a在编译阶段被识别,而在执行阶段,先执行第三行输出a,再为a赋值2。

    事实上,在编译阶段,函数也具有声明提升的过程,且先于变量的声明提升。

  • var变量的重新声明

    作为JS这门语言中的"元老",V8编译器对var十分纵容,即使是在严格模式下,使用var重新声明同名的变量也不会报错,这点与letconst十分不同。

    function testVar() {
      var x = 5;
      if (true) {
        var x = 10; // 这里的x会覆盖外层的x
        console.log(x); // 10
      }
      console.log(x); // 10,因为var变量提升了且在函数内可被覆盖
    }
    

    这个特性似乎一直被人诟病,在复杂的代码块之中,如果开发者粗心地使用var定义(声明并且赋值)一个已经存在的同名变量,并且将其绑定到了某个业务逻辑中,那么将导致未知的错误且难以溯源

let关键字

let在ES6中引出,用于声明块作用域的局部变量,其声明的变量默认值为undefined。

  • 块级作用域

    let(也包括const) 与一对{}结合时,let所在行就会构成一个块级作用域,其声明的变量无法被外部作用域访问。

    function testLet() {
      let y = 5;
      if (true) {
        let y = 10; // 这是另一个y,仅在if块内有效
        console.log(y); // 10
      }
      console.log(y); // 5,因为if中的let形成了块级作用域
    }
    
  • 暂时性死区(Temporary dead zone)

    引入暂时性死区是为了说明let(其实也包括const)声明变量时,V8引擎不会对其执行声明提升,而是将其放进暂时性死区 ,待执行到所在行时才允许访问它们。

    console.log(a);//ReferenceError: Cannot access 'a' before initialization.
    let a = 2;
    

    当console.log()企图访问a变量时,由于a在暂时性死区中,还未被初始化,禁止被访问,故报错。

    我们再来看一个经典的例子,源自MDN

    function test() {
      var foo = 33;
      if (foo) {
        let foo = foo + 55; // ReferenceError
      }
    }
    test();
    

    大家有get到点子吗?准确地说。在调用函数test()时,"=" 所进行的赋值操作(第4行)势必要先访问foo的引用,但是此时foo仍然在暂时性死区当中,直到这句代码执行结束才会离开。

    在使用let、const声明变量时初始化的值不应该包含变量自身。

const关键字

let一起在ES6中引入,用于声明块作用域内的局部变量,定义的变量值不能被二次修改,对象除外,无默认值。

  • 声明常量

    本着追求新技术的准则,我们要学会运用JS中的那些新特性,这些特性大都是原有语法漏洞的补充,能在开发中堪当大用。将原本只能用var声明的常量改为const声明,在一定程度上保证了安全。

    const PROJECT_NAME = 'Dream';
    const PI = 3.1415926;
    

    这样,即使是人为地对常量重新赋值,也会被禁止,不过这一特性似乎在对象上失效了。

    const obj = {
        name:'xiaoming',
        age:18,
        hobby:'programming'
    }
    
    obj.age = 20;
    
    console.log(obj);//{ name: 'xiaoming', age: 20, hobby: 'programming' }
    

    上面我们使用const定义了一个对象,const 定义的对象其属性默认是可以修改的,这点对数组对象同样适用。我们还。可以通过Object.freeze()来冻结一个对象,保证其不能被二次修改,细节在本文后半部分我会详细梳理。

  • 初始化要求var、let不同的是,当我们使用const声明一个常量后 必须 要对其赋值,V8引擎不会给予其默认值undefined,而是直接抛出一个错误。

那些你不知道细节

总讲定义,好像太繁杂,那我们就来看看那些鲜为人知的细节:

  • 面试官:如何用var定义一个常量

    如果在面试的时候遇到这个问题,你会不会怀疑自己的听力呢?var还能定义一个常量?

    没错!我们要知道,在ES6发布(2015年)之前,是没有const关键字的,那个时候的项目中如何去创建一个常量呢?这么一想,好像还真有了解的必要,毕竟以后去到一个公司,总有些代码是按照以前的规范写的,我们要想读懂就得知道这些细节。

    var PI = 3.1415926;
    
    

    事实是,通常我们用var去定义一个常量时,会将常量名大写,这是一种编程习惯,开发者遇见大写的常量,自然不会去修改,不过目前使用const定义显然是一种更合理的方式。

    但是对于常量对象,官方为我们提供了一些方法去修改对象的可修改性,让其变得不可修改。

    var obj = {
        name:'xiaoming',
        age:20
    }
    console.log(obj.age);//20
    Object.defineProperty(obj,age,{
        value:19,//此处可以修改值
        writable:false,//禁止修改
    })
    obj.age = 18;
    console.log(obj.age);//19
    

    Object.defineProperty()能够为对象定义一个属性(如果属性未定义过),并提供了修改可见性的配置writable:false;

    等价于:

    var obj = {
        name:'xiaoming',
        age:20
    }
    Object.freeze(obj);
    obj.age = 18;
    console.log(obj.age);//20
    

    Object.freeze()表示冻结一个对象,使其不能被修改。对于const定义的对象,如果需要,我们也可以 "冻结" 它。

面试题

  • 面试官:这题结果是什么

    var arr = []
    
    for(var i = 0 ; i < 10 ; i++){
        arr[i] = (function(){
            return console.log(i);
        })
    }
    
    arr.forEach(function(item){
        item();
    })
    

    这里使用一个for循环为arr数组赋值,但是值得注意的是,返回的console.log(i)语句并没有立即执行,而是在for循环执行完之后再通过数组的forEach()方法调用打印。我们应当知道,var上声明的变量i的词法作用域是全局作用域,也就是说forEach()访问的是全局下的i,for循环结束之后i的值为10,故结果是打印10个10。

    item类型为函数,执行到item()这句代码时,就会调用arr数组中函数,打印i。

  • 面试官:那如何修改,让结果为0-9

    我们仔细观察会发现,因为var声明的变量i会有一个声明提升的过程,导致打印的全是同一个在全局作用域中的变量i。那么,我们只需要让其打印的分别是10个不同的变量i就成功了。

    var arr = []
    
    for(let i = 0 ; i < 10 ; i++){// 使用let声明,每个i都在单独的词法作用域
        arr[i] = (function(){
            return console.log(i);
        })
    }
    
    arr.forEach(function(item){
        item();
    })
    

    这里将var改为let就解决了问题,这是因为let{}形成了一个块级作用域,每次循环产生的变量i都在一个单独的块级作用域,彼此独立。

    面试官听了你的解释,点了点头,意味深长地继续问道:不使用let,还有别的方法吗?

    你:emmmm,能给点提示吗?

    面试官:你上面使用了块级作用域的特性,再想想有没有类似的方式。

    你瞬间想到了函数作用域,但是要求不使用let,意味着还得使用var定义变量,好像只有...闭包,将数组赋值过程放在一个函数作用域中,每次for循环就结束一次函数的调用。具体是这样:

    var arr = []
    
    for(var i = 0 ; i < 10 ; i++){// 使用let声明,每个i都在单独的词法作用域
        (function(j){
            arr[j] = (function(){
                console.log(j);
            })
        })(i)
    }
    
    arr.forEach(function(item){
        item();
    })
    

    在这里我们使用立即执行函数(IIFE) 给数组赋值,这里巧妙地运用了闭包的特性。在for循环中每次都i传给j执行一次立即执行函数,但是其内还有一个内部函数被赋给了数组arr,当立即执行函数调用结束后,内部函数对j的引用仍然在闭包中。即使外部函数(立即执行函数)调用结束,使用forEach()时也能准确地拿到每一个闭包里的变量j

    😊至此,你完美地回答出了这一题。面试官面带微笑,继续给出了下一个让你头疼的难题。

总结

本文梳理了:

  • 基本数据类型
  • var、let和const的特性
  • var和let结合作用域、闭包在面试题中的考点

写文不易,如果你觉得对你有帮助,那么点个小赞,这将是我继续创作的动力,感谢!

参考:
MDN Web Docs (mozilla.org)

本人拙见,若有错误,敬请指正。