JavaScript 基础之变量声明

333 阅读11分钟

这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战

前言

了解 JavaScript 的人比比皆是,理解 JavaScript 的人寥寥无几。

本文将带着大家一起深入了解 JavaScript 基础之变量声明,废话少说,那就开始吧。

目录

可以看下下面的思维导图

变量声明有哪些?

变量声明的方式有:

  • var:声明变量
  • let:声明变量
  • const:声明常量
  • function:声明一个函数
  • import:导出一个模块
  • class:声明一个类

var 声明

为什么会出现 var 声明?

我们先来看看不使用 var 声明,可能会出现什么问题?

在控制台输入以下代码

myName = '追梦玩家';

console.log(window.myName); // 请问,这个结果是?

function fn() {
    myName = '追梦玩家1';
}
fn();
console.log(window.myName); // 请问,这个结果是?

结果,可以看下截图

有个疑问,为什么我在 fn 函数里面给 myName 进行赋值 "追梦玩家1",执行完 fn 函数,打印 window.myName,打印出的结果是“追梦玩家1”?

其实这就是涉及到一个概念,就是不使用 var 声明的变量,是全局变量,全局变量其实就是在 window 对象中,添加属性并赋值。

最重要的一点,就是如果在函数里面,执行 myName = '追梦玩家1'; 这个代码,相当于重新设置下 window.myName,这样的话,有可能覆盖之前声明的变量值。

// myName = '追梦玩家1'; // 相当于直接给 window 添加属性并赋值
window.myName = '追梦玩家1';

var 声明变量的优缺点

相信大家平时声明变量,都是使用 let 或者 const 声明,那为什么还要说下 var 声明变量的优缺点呢?

以前使用 var 声明变量,可能是有它存在的意义。而新特性的出现,往往都是为了解决某些问题而出现。

如果只知其然而不知其所以然,只是去背用法,但不知道为什么需要这样用,不仅不能融会贯通,而且也没有办法促使自己进步。

因此在下面介绍 let 和 const 之前,我们要先了解下使用 var 声明有什么优缺点,才能理解为什么会有 let 和 const 的出现。

优点

解决函数执行,会覆盖之前声明的变量

我觉得使用 var 声明变量,还是有优点,只不过不多。比如,上面在函数里面,不使用 var 声明的变量,会覆盖之前声明的变量值。

使用 var 声明,可以解决“函数执行,覆盖之前的声明变量”的问题

function fn() {
    var myName = '追梦玩家';
}
fn();
console.log(window.myName); // 请问,这个结果是?

相信大家动手尝试下,都可以知道,这个结果是 "",而不是 “追梦玩家”。但是在全局作用域下,使用 var 声明变量,还是相当于给 window 添加属性,那有办法解决吗?请看下面使用 var 声明的缺点。

缺点

在全局作用域下,var 声明会给 window 设置属性

在全局作用域下声明一个变量,也相当于给 window 全局对象设置了一个属性,变量的值就是属性值(私有作用域中的声明的私有变量和 window 没啥关系)简单来说,全局变量和 window 中的属性存在“映射机制” 一个被修改,另外一个也跟着变 。

在控制台输入以下代码,看下输出结果

// 说明会给 window 设置属性
var myName = '追梦玩家';
console.log(window.myName); // 请问,这个结果是?

// 说明:全局变量和 window 的属性,会存在“映射机制”
window.myName = '砖家';
console.log(myName); // 请问,这个结果是?

如果大家理解“映射机制”的话,就很好懂了,就是给 myName 或者 window.myName 赋值,window.myName 和 myName 都会发生改变。

结果,可以看下截图

有办法解决这个问题?有的,可以通过立即执行函数来实现

立即执行函数的作用,就是创建一个独立的作用域,这个作用域里面的变量,外面访问不到,即避免「变量污染」。

// 立即执行函数,其实就是声明一个函数,然后马上执行
!function() {
    var myName = '追梦玩家';
}();
console.log(window.myName); // 输出结果?
console.log(myName); // 输出结果?
存在变量提升

下面通过一个例子,来理解

console.log(myName); // 输出结果?是否会报错?
var myName = '追梦玩家';
console.log(myName); // 输出结果?

相信大家,在学习 JavaScript 基础的时候,都有遇到过类似的代码。为什么在声明 myName 变量之前,打印 myName,不会报错,输出结果是 undefined,这就是涉及到 JavaScript 一个概念:变量提升。

注:使用 function 声明的函数,也会有变量提升。

想了解“变量提升”,可看这篇文章:我知道你懂 hoisting,可是你了解到多深?

允许重复声明

即使在同一个作用域,同样名称的变量,也允许重复声明,不会出现任何错误或者警告,很容易忽略自己有声明过这个变量。

值得注意的是,重复声明一个变量,并不会重新设置这个变量的值。

举个例子,比如一段很长的代码,我们可能不记得前面有声明过变量,后面再次使用 var 声明,当做第一次声明,容易造成小 Bug。

var myName = '追梦玩家';
// 写了很多代码之后
// ...
// ...
// ...
var myName;
while(true){
    if( name === undefined ){
         console.log('The first time to execute.'); // 这里会被执行吗?
    }
    // ...
}
没有块级作用域

先看一下这个例子:

function fn() {
  {
  	var myName = '追梦玩家';
  }
  console.log('fn(): myName=', myName);
}
fn();

执行结果:

fn(): myName= 追梦玩家

使用 var 声明的变量并不具备块级作用域的效果。

而块级作用域特性在其他编程语言很常见,es6 出现的块级作用域,其实就是参考其他编程语言,借鉴优秀经验。

不支持声明常量

常量指的是「固定不变的值,无法重新修改的」。

在程序里面,有时候,有些值是只需要声明一次,不需要也不希望在程序执行的过程中被修改。

例如,数学的 PI 是 3.14,如果是程序中某个地方修改了这个值,就会导致结果出现错误。

var PI = 3.14;
PI = 1024;

但是使用 var 声明,没有办法做到这一点,你想怎么改变值都是可以,程序代码执行,会存在不确定性,有风险。

许多其他编程语言,对于常量,都有提供管控机制来提高安全性,一旦误该,可能编译阶段就能发现,甚至在 IDE 在编写代码阶段就能提醒,减少 debug 负担。

let 声明

为什么会出现 let 声明?

let 跟 var 的作用差不多,但有着非常重要的区别。最明显的区别是,let 声明的范围是块级作用域,而 var 声明的范围是函数作用域。let 是在 ES6 版本出现的,其实就是为了解决使用 var 声明变量,遇到的问题,或者说是缺点。具体可以继续往下看。

let 解决了什么问题?

全局声明,不会成为 window 对象的属性

可以看下下面的例子:

var myName = '追梦玩家';
console.log(window.myName); // 输出结果?

let age = 18;
console.log(window.age); // 输出结果?
window.age = 30;
console.log(age); // 输出结果?

具体结果,请看截图

说明一下,上面代码为什么给 window.age 进行赋值呢?因为使用 var 声明的话,会跟 window 的属性存在“映射机制”,所以要这样去测试下。

修改了 window.age,打印 age,还是 18,所以得出结论:使用 let 在全局作用域中声明的变量不会成为 window 对象的属性。

没有变量提升

看下下面的例子:

console.log(myName); // 输出结果?
let myName = '追梦玩家';

如果是使用 var 声明的话,输出的结果肯定是 undefined,但是使用 let 声明的话,是输出 undefined,还是报错?

结果当然是报错啦。因为 let 是没有变量提升,在声明之前访问 myName 变量的话,就出现报错:

不允许重复声明

看下下面的例子:

let myName = '追梦玩家';
// ... 写了好多代码
let myName;

在控制台,执行上面的代码,会出现报错,浏览器说,myName 已经被声明过了,你为什么还要声明,给个语法报错,自己看看吧。

我们回忆下,如果是使用 var 声明的变量,是否会出现报错呢?打印 myName 变量的值是?

不会有报错,myName 是追梦玩家。

存在块级作用域

首先先了解下,什么是块级作用域?

块级作用域就是使用一对大括号包裹的一段代码,比如函数、判断语句、循环语句,甚至单独的一个{}都可以被看作是一个块级作用域。

//if块
if(1){}

//while块
while(1){}

//函数块
function foo(){
 
//for循环块
for(let i = 0; i<100; i++){}

//单独一个块
{}

简单来说,块级作用域里面的定义的变量,在代码块外部是访问不到的。

理解上面这句话,就知道执行下面的代码,其实会报错:Uncaught ReferenceError: myName is not defined

{
    let myName = '追梦玩家';
}
console.log(myName); // 输出结果?

特性

暂时性死区

我们先来了解下,暂时性死区的作用:

ES6 规定暂时性死区和 let 、const 语句不出现变量提升,主要是为了减少运行时的错误,防止在变量声明前就是用这个变量,从而导致意料之外的行为。

暂时性死区:只要块级作用域内存在 let、const 命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。

结合下面的例子来理解下:

// var num = 1;
if (true) {
  num = 'a'; // Error: Uncaught ReferenceError: Cannot access 'num' before initialization
  let num;
}

因为在 if 中声明了一个局部变量,导致出现暂时性死区,if 里面的 num 则与这个块级作用域捆绑在一起,不在受全局变量 num 的影响,同时 let 不存在变量提升,所以在 let 前赋值的 num 是非法的。那个报错,就好像是浏览器说:不要在声明之前使用 num。const 与之同理

其本质就是,只要进入当前作用域,所要使用的变量就已经存在,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

条件声明

因为 let 的作用域是块,所以不可能检查前面是否已经使用 let 声明过同名变量,同时也就不可能在没有声明的情况下声明它。

看下下面这个例子:

if (typeof name === 'undefined') { 
    let name; 
}
// name 被限制在 if {} 块的作用域里面
name = '追梦玩家'; 
// 执行上面这行代码,相当于是给 window 添加一个属性 name,进行赋值,而不是给变量 name 赋值,
// 因为变量 name 被限制在代码块的作用域里面

const 声明

const 的行为与 let 基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且尝试修改 const 声明的变量会导致运行时错误。

上面这个区别,要怎么理解呢?

可以看下这个例子:

// 如果使用 const 声明,后面进行修改,会出现报错
const myName = '追梦玩家';
myName = '砖家'; 

这个报错,就好像是浏览器说:不可以给常量赋值,都说了是常量,懂不。

大家有可能看到过类似的代码

const person = { name: '追梦玩家' };
person.name = '砖家';
person.age = 18;
console.log(person); // 输出结果?

并没有出现报错,而是输出一个对象。

这其实就是涉及到一个问题:const 定义的对象属性是否可以改变

person 对象的 name 属性确实被修改了,也可以给 person 对象添加属性,怎么理解这个现象呢?

因为对象是引用类型,person 中保存的仅仅是内存地址,也就是 const 声明的限制只适用于它指向的变量的引用。

简单来说,怎么理解内存地址呢?

内存地址,就好比就是仓库的钥匙,因为对象,是可以往里面添加、修改属性,就好比如,是一个大仓库。你有仓库的钥匙,就可以往里面放东西。

换句话说,使用 const 定义的对象,是可以修改这个对象内部的属性的。

function

为什么会出现函数?

function 关键字,创建函数,其实函数名也是变量,只不过存储的值是引用数据类型而已。

在JS中,函数就是一个方法(一个功能体),基于函数一般都是为了实现某个功能「比如洗衣机就是一个函数」,为什么说洗衣机是一个函数呢?

其实,洗衣机提供入口,给你放衣服和洗衣粉之后,洗衣机将衣服洗干净之后,给你的是干净的衣服。提供入口,就相当于是函数的参数,给你干净的衣服,就相当于函数处理完某些操作,返回一些东西给你。

var total = 10;
total += 10;
total = total /2;
total = total.toFixed(2);

/* 上面这个代码,就是想做下简单的数学运算,如果在后续的代码中,
我们依然想实现相同的操作(加10除以2),我们是要复制代码,粘贴代码,
这样的话,就会导致页面中存在大量冗余的代码,也降低了开发效率。
如果我们能把实现这个功能的代码进行“封装”,后期需要这个功能,调用方法即可,方法也就是函数。*/

函数诞生的目的就是为了实现封装:把实现一个功能的代码封装到一个函数中,后期想要实现这个功能,只需要把函数执行即可,不必要再重复编写重复的代码,起到了低耦合高内聚(减少页面中的冗余代码,提高代码的重复使用率)的作用。

function fn(){
	var total = 10;
    total += 10;
    total /= 2;
    total = total.toFixed(2);
    console.log(tatal);
}
fn();
fn();
...
// 想用多少次,我们就执行多少次函数即可

创建函数

ES3 标准

/*
// => 创建函数
function 函数名([参数]){
    函数体:实现功能的JS代码
}
// => 把函数执行
函数名();
*/

// 创建函数
function fn() {
	console.log('hi');
}
fn(); // 函数执行

ES6 标准中创建箭头函数

/*
let 函数名(变量名) = ([参数])=>{
    函数体
} 
函数名();
*/

let fn = () =>{
    let total = 10;
    // ...
}
fn();

函数的形参和实参

参数是函数的入口:当我们在函数中封装一个功能,发现一些原材料不确定,需要执行函数的时候用户传递进来才可以,此时我们就基于参数的机制,提供出入口。「比如是生产洗衣机的厂家,并不知道用户是怎么操作,他们只需要提供一些孔,入水口」

//=>此处的参数叫做形参:入口,形参是变量(n / m 就是变量)
function sum(n,m){
    //=> n 和 m 分别对应 要求和的两个数字
    var total = 0;
    total = n + m;
    console.log(total);
}

// => 此处函数执行传递的值是实参:实参是具体的数据值
sum(10,20);  // => n=10 m=20
sum(10); //=> n=10 m=undefined
sum(); //=> n 和 m 都是undefined
sum(10,20,30);  // => n=10 m=20 30没有形参变量接收

简单来说,函数调用的时候,传递的是实参。创建的函数的时候,圆括号里面的参数,是形参。

函数的 return

我们先来看看这个需求:在函数外面获取到 total 的值,要怎么获取呢?这就要用到 return 啦。

function fn(n,m) { 
    var total = 0; // => total:私有变量
    total = n + m;
    return total; // => 并不是把 total 变量返回,返回的是变量存储的值,return 返回的永远是一个值
}

// 下面先说一下 fn 和 fn() 的区别
fn // => 代表的是函数本身 只创建函数,不去使用,并没有意义
fn(10,20); // => 代表的是函数执行(不仅如此,它代表的是函数执行后,返回的结果[return 返回的值])

var result = fn(1,2); // 函数执行,返回的结果是有 return 的内容决定的
console.log(result); // => 3

return 的作用

  1. 返回值,如果当前函数没有 return 结果出来(或者return;啥也没返回),函数执行在外面拿到的结果都是 undefined
  2. 类似于循环中的 break ,能够强制结束函数体中代码的执行(return 后面的代码不再执行)
function fn(n,m) { 
    var total = 0; // => total:私有变量
    total = n + m;
    return total; // => 并不是把 total 变量返回,返回的是变量存储的值,return 返回的永远是一个值
  	console.log('hi'); // 请问,这里会被执行吗?
}
fn(1, 2);

上面的 console,是不会被执行的,因为,return 后面的代码不再执行的。

不知道大家有没有看到过这样的代码:

function fn() {
	console.log('hi');
}
console.log(fn()); // 是否有返回值?返回的结果是?

执行上面的代码,当然是有返回值,没有显示的使用 return,相当于是默认返回了 undefined。所以上面的返回结果是 undefined。

在每一个 JavaScript 程序中,函数都会是你需要使用的工具,因为他们能提供的一种独特的能力,让你的代码重复使用。在编程的时候,我们无法离开函数,无论是自己构建的函数还是在使用 JavaScript 语言本身带有的函数。

上面讲的是函数的基本用法。后期也会深入了解函数的运行机制、一些比较进阶的函数特性之类的。

import

我们了解下 import 的简单用法,首先要知道什么是模块?

什么是模块?

一个模块(module)就是一个文件。一个脚本就是一个模块。就这么简单。

模块可以互相加载,并可以使用特殊的指令 export 和 import 来交换功能,从另一个模块调用一个模块的函数:

  • export 关键字标记了可以从当前模块外部访问的变量和函数
  • import 关键字允许从其他模块导入功能

比如,我们有一个 utils.js 文件导出了一个工具函数:

// utils.js
// 格式化时间
export function formatTime(time) {
  if (typeof time !== 'number' || time < 0) {
    return time;
  }

  const hour = parseInt(time / 3600, 10);
  time %= 3600;
  const minute = parseInt(time / 60, 10);
  time = parseInt(time % 60, 10);
  const second = time;

  return ([hour, minute, second]).map(function(n) {
    n = n.toString();
    return n[1] ? n : '0' + n;
  }).join(':');
}

然后我们在其他模块中需要使用到这个函数,所以使用 import 进行导入使用。

// main.js
import { formatTime } from './utils.js';

formatTime(160); // "00:02:40"

class

以前生成实例对象的传统方法,是通过构造函数。ES6 引入了 Class 这个概念,作为对象的模板。通过 class 关键字,可以定义类。

简单来说,class 就是语法糖,糖是 sweet,让你心里甜甜的感觉。能让你少写一些代码,但是只用语法糖,不学语法糖背后的原理,我是反对的。

依然以最简单的代码为例,这就是 class。

class Human{
  sayHi(){
    console.log('hi')
  }
}

后期,我会单独写一篇文章,会结合原型相关的知识,深入了解 class 语法糖背后的原理。

学会了 prototype,再去学 class 就跟吃糖一样简单。

参考资料