这是我参与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 的作用
- 返回值,如果当前函数没有 return 结果出来(或者return;啥也没返回),函数执行在外面拿到的结果都是 undefined
- 类似于循环中的 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 就跟吃糖一样简单。