重新认识JavaScript(一)

459 阅读4分钟

上一篇:重新认识JavaScript(零)

上一篇文章中,介绍了JavaScript的一些概念,以及在HTML页面中如何使用JavaScript。这一篇,开始了解JavaScript的基础知识。

什么是变量

变量,就是用于存放数值的容器。

这个数值可以是一个可以用于累加计算的数字,或者是一个句子中的字符串。变量的独特之处在于它存放的数值是可以改变的。先来看一个简单的例子:

<button>点我</button>
// querySelector返回文档中与指定选择器或选择器组匹配的第一个HTML元素
const button = document.querySelector('button');

button.onclick = function () {
    /*
        prompt方法用于显示可提示用户进行输入的对话框,返回用户输入的字符串
        取消返回null
    */
    let name = prompt('你叫什么名字?');
    alert(`欢迎你,${name}!`);
};

在这个例子里面,第一行代码会弹出一个输入框,让用户输入一个名字,然后将这个名字存在一个变量中。第二行代码会弹出一个带有刚才输入名字的信息。变量name则存储了刚才用户输入的名字。

变量的另一个特性是可以存储任何东西,不止是数字和字符串。变量可以存储更复杂的数据,甚至是函数。

这里仍有一个重要的概念需要区分。变量不是数值本身,它仅仅是一个用于存储数值的容器。

声明变量

想要使用一个变量,那么第一件事当然是创建它。更专业的说法是声明一个变量。声明一个变量的方法是在varlet关键字后面加上这个变量的名字:

var myName;
let myAge;

提示:在JavaScript中,所有代码指令都会以;结尾。如果忘记加分号,你的单行代码可能执行正常,但是多行代码在一起可能会执行出错。所以最好养成主动以分号区分代码结尾的习惯。

可以通过使用变量的方式来验证这个变量的数值是否在执行环境中已经存在。例如:

myName;
myAge;

以上这两个变量并没有数值,它们是空的容器。当使用这两个变量时,会得到一个undefined的返回值。如果使用一个不存在的变量,则会得到一个报错信息。

提示:“一个变量存在,但它没有数值”和“一个变量不存在”完全是两回事。不存在意味着没有存放数值的容器,另一个则意味着这是一个空的容器。

初始化变量

一旦声明了一个变量,就可以使用它。方法是在变量名后面跟上一个=,再在后面加上数值:

myName = '伊丽莎不白';
myAge = 32;

现在再使用这两个变量,会发现它们已经存储了刚刚赋于它们的数值。同样,也可以在声明变量的时候初始化变量:

let myName = '伊丽莎不白';

var 与 let 的区别

这是JavaScript的一个历史遗留问题,同样也是绝大多数前端开发面试必问的一个问题。值得认真对待。

最初在创建JavaScript时,是只有var的。在大多数情况下,这种方法可以接受,但有时在工作方式上会有一些问题:它的设计会令人困惑。因此let是现代版本JavaScript创建的一个新关键字,用于创建与var工作方式有些不同的变量,解决了过程中的问题。

下面解释这些差异。

定义

首先来看varlet的定义。

  • var声明语句声明一个变量,并可选的将其初始化为一个值。
  • let语句声明一个块级作用域的本地变量,并且可选的将其初始化为一个值。

描述

var变量声明,无论发生在什么地方,都在执行任何代码之前处理。用var声明的变量的作用域是它当前的执行上下文,可以是嵌套的函数,也可以是声明在任何函数外的变量。如果重新声明一个变量,这个变量不会丢失它的值。

变量提升

由于变量声明总是在任意代码执行之前处理的,所以在代码中任意位置声明变量总是等效于在代码开头声明。这意味着可以在变量声明之前使用变量:

myName = '伊丽莎不白';
var myName;

/*
    可以理解为
    var myName;
    myName = '伊丽莎不白';
*/

因此,建议始终在作用域顶部声明变量(全局代码的顶部和函数代码的顶部),这可以清楚的知道哪些变量是函数作用域,哪些变量在作用域链上解决。

重要的是,提升仅影响变量声明,而不会影响其数值的初始化:

var a = 'A';
// => A
var b = a;
// => A
var b = a;
// => undefined
var a = 'A';
// => A

作用域限制

声明变量的作用域限制在其声明位置的上下文中:

function init () {
    var a = 1;
}

init();

console.log(a);
// => ReferenceError,变量a作用域在init函数内部,未在init函数外部声明

重复声明

可以在代码中根据需要多次声明名称相同的变量:

var myName = '伊丽莎不白';
var myName = '不白';
// => 不白

let允许声明一个作用域被限制在块级中的变量、语句或表达式。与var不同的是,let声明的变量只能是全局或者整个函数块的。

作用域规则

let声明的变量只在其声明的块或子块中可用,这一点与var相似。二者最主要的区别在于var声明的变量的作用域是整个封闭函数。

function varTest () {
    var a = 1;
    if (true) {
        // 变量提升,a为相同的变量
        var a = 2;
        console.log(a);
        // => 2
    }
    console.log(a);
    // => 2
}

function letTest () {
    let a = 1;
    if (true) {
        // a为不同的变量
        let a = 2;
        console.log(a);
        // => 2
    }
    console.log(a);
    // => 1
}

简化内部函数代码

当用到内部函数时,let会让代码变得更简单。下面用一段非常常见的面试代码来举例:

for (var i = 0; i < 5; i++) {
    var btn = document.createElement('button');
    btn.innerHTML = `按钮${i}`;
    
    var j = i;
    btn.onclick = function (evt) {
        console.log(`点击了按钮${j}`);
    };
}

上面这段代码的意图是创建5个按钮,点击不同的按钮能够打印出当前按钮的序号。然而结果将总是打印出点击了按钮5。因为j是函数级变量,5个内部函数都指向了同一个j,而j最后一次的赋值是5。

如果想要实现预期的效果,只需要将j的声明语句改成let。这样j变成了块级域,所以5个内部函数指向了不同的j。

在程序或函数的顶层,let不会像var一样在全局对象上创建一个变量,例如:

var a = 1;
let b = 2;
console.log(this.a);
// => 1
console.log(this.b);
// => undefined

模仿私有接口

在处理构造函数的时候,可以通过let声明而不是闭包来创建私有接口。

{
    let _age = 32;
    
    var Person = function () {
        this.name = '伊丽莎不白';
    };
    
    Person.property.growUp = function () {
        _age++;
        console.log(_age);
    };
}

var p = new Person();
p.growUp();
// => 33
console.log(_name);
// => ReferenceError

这里需要注意一点,因为_name是被Person的实例所持有的,所以在使用中会存在内存泄漏的风险。闭包也是同理。

重复声明

在同一个函数或块作用域中重复声明一个变量会引起SyntaxError。

let myName;
let myName;
// => SyntaxError

switch语句中只有一个块作用域,可能会因此引发错误:

let a = 1;
switch (a) {
    case 0:
        let b;
        break;
    case 1:
        let b;
        // => SyntaxError
        break;
}

然而,一个嵌套在case字句中的块会创建一个新的块作用域,不会产生上面提到的错误:

let a = 1;
switch (a) {
    case 0: {
        let b;
        break;
    }
    case 1: {
        let b;
        // => SyntaxError
        break;
    }
}

暂存死区

let被创建在包含该声明的作用域的顶部,称之为“提升”。与var不同,通过let声明的变量直到它们的定义被执行时才初始化。该变量处在一个自块顶部到初始化处理的“暂存死区”中,这之间对变量的访问会造成错误:

console.log(a);
// => undefined
console.log(b);
// => ReferenceError
var a = 1;
let b = 2;

let后面跟一个函数传递的参数时将导致循环内部报错:

function go (n) {
    for (let n of n.a) {
        console.log(n);
    }
}

go({ a: [1, 2, 3] });
// => ReferenceError

更新变量

一旦变量赋值,还可以通过简单的给它一个不同的值来更新它。

let myAge = 32;
myAge = 18;
// => 18

变量类型

可以为变量设置不同的数据类型,例如:

let myName = '伊丽莎不白';
// => String
let myAge = 32;
// => Number
let isAlive = true;
// => Boolean
let myTels = [10086, 10010];
// => Array
let myCat = {
    name: 'Pie',
    age: 4
}
// => Object

动态类型

与C或Java不同,JavaScript是一种“动态类型语言”,这意味着在使用过程中不需要为变量指定一个数据类型。

声明一个变量并赋给它一个带引号的值,那么浏览器将会知道它是一个字符串:

let myName = '伊丽莎不白';

即时包含数字,依然是一个字符串:

let myAge = '32';
// => String
myAge = 32;
// => Number

总结

到现在为止,对变量已经有了一个较为全面的认识,尤其是对varlet的使用。下一篇文章将继续深入JavaScript,介绍数字与字符串。

下一篇:重新认识JavaScript(二)