变量是每一门编程语言中非常重要的一个概念,不同的编程语言中变量的作用也基本一致。变量可以通过变量名访问,是计算机语言中能储存计算结果或能表示值的抽象概念。
JavaScript变量
JavaScript属于弱类型定义语言,某一个变量被定义类型,该变量可以根据环境变化自动进行转换,不需要经过显性强制转换。
//比如
var a = 1;
a = {x: 1};
console.log(a); //{x:1}
但是像Java、.net、Python、c++等这样的强类型定义语言,上述的操作就会引起程序报错。
JavaScript变量声明(创建)
JavaScript变量命名需要遵循的规则
- 变量必须以字母开头
- 变量也能以 $ 和 _ 符号开头(不过我们不推荐这么做)
- 变量名称对大小写敏感(y 和 Y 是不同的变量)
JavaScript变量声明的几种方式
- es6之前使用var和function进行变量声明
- es6中新增let和const
function也是变量的声明方式吗?
我们来验证一下function是否可以声明变量。
let add = 1;
function add(num1, num2) {return num1 + num2};
//Uncaught SyntaxError: Identifier 'add' has already been declared
思考一下使用var,function,let,const声明变量有什么区别?
在这之前我们要知道在JavaScript中变量声明和赋值(初始化)对于Js引擎来说是分为了两个阶段。一个Js的变量初始化语句也将被解释成两个语句来执行,比如
var a = 1;
//被Js引擎解释为
var a; //变量声明语句
a = 1; //变量赋值语句
我们知道了这个过程后继续探索JavaScript变量操作相关的特性。
-
变量声明提升
- var 和 function有变量声明提升的功能
- let 和 const 则没有变量声明提升的功能,必须要先声明才能使用
//验证变量提升
//1. 如果不进行声明直接使用会报错
console.log(a); //Uncaught ReferenceError: a is not defined
//2. 先使用后声明
console.log(a); //undefined
var a = 1;
//对于js引擎来说和下面的情况是一样的
console.log(a); //undefined
var a;
//或者
var b;
console.log(b); //undefined
//3. 先声明并赋值后使用
var a = 1;
console.log(a); // 正常输出 1
//4. 使用function声明函数变量
add(1, 2); // 正常输出 3
function add(num1, num2) {
console.log(num1 + num2);
}
//5. 使用let
console.log(a); //Uncaught ReferenceError: a is not defined
let a = 1;
//6. 使用const
console.log(a); //Uncaught ReferenceError: a is not defined
const a = 1;
从上面几个例子中可以看出,如果在程序中未使用var和function对一个变量进行声明直接使用则会报错;如果声明了变量但未赋值或者初始化,则该变量的默认值为undefined;如果变量赋值或者初始化在使用之后,则在使用变量时其值也为undefined。使用var和function声明的变量在当前执行环境下,即使调用该变量的地方其值为undefined,也能说明该变量是存在的(也就是说该变量被声明了)。而使用let和const声明的变量并不能达到这样的效果。
思考一个问题,变量声明提升到底是一个什么过程?
我们知道JavaScript是解释执行语言,和Java那种编译执行语言不同。说到这里我们需要知道JavaScript代码的执行其实分为两个阶段—解释阶段(也可以理解为编译阶段,但是和Java那种编译成二进制的过程不一样)和执行阶段。在解释阶段,Js引擎会对当前环境下所有的变量声明(注意,此处是声明而不是赋值)放到了程序的最前端。下面看一看几段JavaScript代码经过Js引擎解释后执行时的真正顺序。
console.log(a);
var a = 1;
console.log(a);
//被Js引擎解释后的执行顺序
var a; //a的值为undefined
console.log(a); // undefined
a = 1; // a的值为1
console.log(a); // 1
add( 1, 2);
function add(num1, num2) {
console.log(num1 + num2);
}
//被Js引擎解释后的执行顺序
function add(num1, num2) {
console.log(num1 + num2);
}
add(1, 2);
到这里我们应该对使用var声明的变量提升和function声明的函数变量提升有了一定的理解。那么,思考一下下面几段代码的输出结果是什么呢?
//第一段代码
var a = 1;
function a {
return 2;
}
console.log(a);
//第二段代码
var b;
console.log(b);
function b(){
return 3;
}
b = 4;
console.log(b);
//第三段代码
var c;
console.log(c);
c = 5;
console.log(c);
function c() {
return 6;
};
c();
console.log(c);
//第四段代码
var d;
console.log(d);
d = 7;
console.log(d);
var d = function() {
console.log(8);
}
d();
console.log(d);
感觉怎么样?大脑有没有点懵?
上面的几段代码中普通变量和函数变量使用了相同的变量名,在我们平时写程序是不推荐这样的,但是你作为一名JavaScript程序员你应该能分析其执行顺序,这也是我们前端开发面试过程中经常考察的问题。下面我们看看上面三段代码经过Js引擎解释之后真正的执行顺序。
//第一段代码被Js引擎解释之后的执行顺序
var a;
function a() {
return 2;
}
a = 1;
console.log(a); // 1
//第二段代码被Js引擎解释之后的执行顺序
var b;
function b() {
return 3;
}
console.log(b); // ƒ b(){ return 3;}
b = 4;
console.log(b); // 4
//第三段代码被Js引擎解释之后的执行顺序
var c;
function c() {
return 6;
}
console.log(c); // ƒ c(){ return 6;}
c = 5;
console.log(c); // 5
c(); // 报错 Uncaught TypeError: c is not a function
console.log(c); // 不会被执行
//第四段代码被Js引擎解释之后的执行顺序
var d; //此处是第一个变量声明
var d; //此处是函数表达式的变量声明提升
console.log(d); //此处d的值为undefined
d = 7;
console.log(d); // 7
d = function() {
console.log(8);
}
d(); // 8
console.log(d); // ƒ (){ console.log(8);}
- 重复声明
- var 和 function能重复声明,后者覆盖前者
- let 和 const 则不能重复声明
var a = 1;
var a;
console.log(a); // 1 如果不明白为什么是1,仔细看上一部分
let a = 1;
let a;
console.log(a); //报错 Uncaught SyntaxError: Identifier 'a' has already been declared
- 作用域的范围
- var 定义的变量的作用域是以函数为界限
- let 和 const 是块作用域(常见为 for 和 if 语句的大括号为界限)
- var 可以定义全局变量和局部变量,let 和 const 只能定义局部变量
如何理解var定义的变量没有块级作用域,let 和 const 声明的变量有块级作用域?
看下面的程序
function fn() {
var count1 = 0;
var count2 = 0;
for(var i=0;i<10;i++) {
count1++
}
for(let j=0;j<10;j++) {
count2++
}
/*if(true) {
let k = 'hello';
}*/
console.log(i); // 10
console.log(j); // 报错 Uncaught ReferenceError: j is not defined at fn
//console.log(k); // 报错 Uncaught ReferenceError: k is not defined at fn
}
fn();
- const 的特殊之处
- 使用const声明的基本数据类型一旦声明其值不可更改,引用类型比较特殊,其内部属性的值可更改。
//使用const定义变量,如果其值为基本数据类型,则值不可更改
const a = 1;
a = 2; // 报错 Uncaught TypeError: Assignment to constant variable.
//使用const定义的变量,如果其值为引用数据类型,则该变量内部属性的值可更改
const obj = {x: 1};
obj.x = 2;
console.log(obj.x); // 2
const arr = [1];
arr.push(1);
console.log(arr); // [1, 1]
JavaScript变量使用(赋值)
声明一个变量,就是为了在这个变量中存储一些值。
还记得JavaScript变量的值都有哪些数据类型吗?
简单数据类型:number,string,boolean,null(特殊类型),undefined(特殊类型)
引用数据类型:object
es6新增数据类型:symbol
存储简单数据类型的变量和存储引用类型的变量在使用过程中需要注意什么?
看一下下面的程序
var age= 22;
var job = 'programer';
var obj = {name: "zhang san"};
var arr = [1, 2];
var fn = function(age, obj, arr) {
var newAge = age;
var newObj = obj;
var newArr = arr;
newAge = 23;
newObj.name = "Jack";
newArr.push(3);
};
fn(age, obj, arr);
console.log(age); // 22
console.log(obj); // {name: "Jack"}
console.log(arr); // [1, 2, 3]
上面的程序相信大部分前端程序员都能知道是什么原因导致的。
简单数据类型,其在内存中分别占有固定大小的空间,他们的值保存在栈空间,我们通过按值来访问。
引用类型,由于其值的大小不固定,因此不能把它们保存到栈内存中。但内存地址大小的固定的,因此可以将内存地址保存在栈内存中。 这样,当查询引用类型的变量时, 先从栈中读取内存地址, 然后再通过地址找到堆中的值。我们按其引用访问。
如果理解的不是很好,可以详细读前端高质量知识(一)-JS内存空间详细图解这篇文章
思考!函数传参中包括引用类型变量,在函数执行后如何保证外部的引用类型变量不被修改?
对!深拷贝。后续文章中会详细介绍。
JavaScript变量类型检测
说起变量类型检测,感觉和变量声明提升一样,又有好多说不完的话了。先思考两个问题吧。
使用typeof variable 得到的值都有哪些?这些值和上文中提到的JavaScript数据类型有什么关系?
来,看下面的程序。
var a = 1;
var b = "hello Jack";
var c = true;
var d = null;
var e = undefined;
var obj = {name: "Jack"};
var arr = [1, 2];
var date = new Date();
var pattern1 = /hello/g;
var pattern2 = new RegExp("[bc]at", "i");
var fn = function(msg) { alert(msg);};
/*es6*/
var s = Symbol();
...
typeof a; // "number"
typeof NaN; // "number"
console.log(typeof b); // "string"
console.log(typeof c); // "boolean"
console.log(typeof d); // 注意 "object"
console.log(typeof e); // "undefined"
console.log(typeof obj); // "object"
console.log(typeof Object); // "function"
console.log(typeof arr); // "object"
console.log(typeof Array); // "function"
console.log(typeof date); // "object"
console.log(typeof pattern1 ); // "object"
console.log(typeof pattern2); // "object"
console.log(typeof RegExp); // "function"
console.log(typeof fn); // "function"
console.log(typeof Function); // "function"
console.log(typeof window); // "object"
console.log(typeof Math); // "object"
console.log(typeof s1); // "symbol"
...
使用typeof进行类型检测的情景差不多了吧。但是使用typeof对变量进行类型检测时有些情况下得到的值并不是我们预想的,比如 typeof null,可以说是JavaScript里面的一个bug,但是这个bug也不会被纠正了,所以记住就可以了。
使用typeof Object 为什么得到"function" ?
在JavaScript中,Object和Array等都是构造函数,构造函数也是函数,所以使用typeof得到的值是"function"是可以理解。
使用typeof检测得到的"function" 是变量的数据类型吗?
注意!typeof是JavaScript提供的一种检测变量类型的方法,此处的变量类型是为了给Js引擎分析的,所以它把object和function给区分开了,和上文提到的JavaScript变量值的数据类型不完全同。上文描述的变量值的数据类型是相对于内存方面有更重要的信息(比如存放于栈内存或者堆内存)。
为什么使用typeof检测存放于堆内存中的“对象”得到的值有的是“object”,有的是“function”?
JavaScript中的对象分为普通对象和函数对象。这两种对象都属于引用类型,并存放于堆内存中。这两种对象会在后续文章中深入讨论。
怎么判断一个对象是另外一个对象(在Java中称为类)的实例?
是的,你应该很快的就能想到instanceof。
JavaScript变量销毁
变量中一旦存了值,它就会占用相应的内存空间。变量中存的是简单数据类型则存储在栈内存中,如果是引用类型则存储在堆内存中。
销毁,是指从内存中回收该变量的存储空间
那么变量什么时候会被销毁(回收)?
如果是全局变量即在window环境下,当浏览器窗口关闭后该变量被销毁(回收)。
如果是局部变量(函数内部变量),当函数执行完之后即被销毁(回收)。
如果是使用let 和 const定义的应用在块级作用域中的变量,则在当前块执行完之后被销毁(回收)。
思考!什么样的情况下函数内部的变量会一直保存在内存当中?
是的,你没有猜错。使用闭包的情况下,函数内部的变量不会在函数执行完之后立即回收,它们会一直留在内存中。关于JavaScript闭包会在后续文章中详细讨论。
小结
变量是编程语言的根基,我们前端程序员应该熟练掌握JavaScript变量的各个特性,尤其是变量声明提升和类型检测。写到这里,关于JavaScript变量相关的知识点聊的差不多了。其实每个知识点如果深入挖的越深则涉及的范围越来越广。在这篇文章中尽量做到点到为止。
这是我在简书上写的第一篇总结性文章,文章结构以及文字表达方面经验不足。如有遗漏或描述错误的地方请各位同学留言指正。