前言
在我们学习各种编程语言时,最先接触的就是变量和数据类型,那么我们真的理记住和理解了这些最基础的知识点吗?我们是否在笔试和面试的时候,经常对一些不起眼的知识点想不起来?那么跟着这篇文章重新回顾下这些基础内容吧。
1 变量(var、let、const)
什么是变量
- 变量是用于进行保存任意值的命名占位符。
- 字母数字下划线美元符,严格区分大小写,首字符不能是数字。
1.1 var关键字
- var关键字不初始化的情况下,会自动保存一个特殊值undefined
- var关键字在函数内部进行定义变量,会成为此函数的局部变量,即作用域是函数作用域。(变量只在当前函数内部有效,退出函数时会自动销毁)
- 在函数内部定义变量时省略var关键字,变量会成为全局变量。(在严格模式下会报错,且难维护)
- var关键字声明的变量会自动提升到函数作用域顶部。(即所有变量声明都拉到函数作用域的顶部)
- var关键字可以重复声明同一个变量。(同一作用域下,最后声明的变量会覆盖之前的变量赋值)
运行代码:
// var关键字不初始化的情况下,会自动保存一个特殊值undefined
var test1;
console.log(test1);//undefined
// var关键字在函数内部进行定义变量,会成为此函数的局部变量。(变量只在当前函数内部有效,退出函数时会自动销毁)
function func1(){
var test2 = "yichuan";
console.log(test2);//yichuan
}
func1();
console.log(test2);//ReferenceError: test2 is not defined
// 在函数内部定义变量时省略var关键字,变量会成为全局变量。(在严格模式下会报错,且难维护)
function func2(){
test3 = "yichuan";
console.log(test3);//yichuan
}
func2()
console.log(test3);//yichuan
// var关键字声明的变量提升
function func3(){
console.log(test4);//undefined
var test4 = "yichuan";
}
func3();
// var关键字可以重复声明同一个变量,同一作用域下,最后声明的变量会覆盖之前的变量赋值
function func4(){
var test5 = "yichuan1";
var test5 = "yichuan2";
var test5 = "yichuan3";
console.log(test5);//yichuan3
}
func4();
1.2 let关键字
- let关键字不初始化的情况下,会自动保存一个特殊值undefined
- let关键字的作用域是块作用域,即:function(){}、if(){}等(所有包含{}的语句)
- let关键字声明的变量不存在变量提升
- let关键字不能在同一作用域下重复声明同一个变量
- let关键字在全局作用域下声明的变量不会成为window的属性,而var关键字声明的变量会出现此情况
运行代码:
// let关键字不初始化的情况下,会自动保存一个特殊值undefined
let test1;
console.log(test1);//undefined
// let关键字的声明作用域属于块级作用域
function func1(){
let test2 = "yichuan";
console.log(test2);//yichuan
}
func1();
console.log(test2);//ReferenceError: test2 is not defined
if(true){
let test3 = "yichuan";
console.log(test3);//yichuan
}
// let关键字声明的变量不存在变量提升
function func2(){
console.log(test4);//ReferenceError: Cannot access 'test4' before initialization
let test4 = "yichuan";
}
func2();
// let关键字在同一作用域下,不能重复声明同一变量
function func3(){
let test5 = "yichuan1";
let test5 = "yichuan2";
console.log(test5);//SyntaxError: Identifier 'test5' has already been declared
}
func3();
// let关键字在全局作用域下声明的变量不会成为window的属性,而var关键字声明的变量会出现此情况
var test6 = "yichuan1";
console.log(window.test6);//yichuan1
let test7 = "yichuan2";
console.log(window.test7);//undefined
1.3 const关键字
- const关键字声明变量必须进行初始化赋值
- const关键字声明的变量不能更改赋值,适用于指向变量的引用(即const变量引用的是对象的话,是可以更改内部属性的)
- const关键字不存在变量提升
- const关键字的作用域也是块级作用域
- const关键字在同一作用域下不能重复声明同一个变量
运行代码:
const关键字声明变量必须进行初始化赋值
const NUM;
console.log(NUM);//SyntaxError: Missing initializer in const declaration
const PI = 3.14;
console.log(PI);//3.14
// const关键字声明的变量不能更改赋值
const TEST = "YICHUAN";
TEST = "ZHENSHUAI";
console.log(TEST);//TypeError: Assignment to constant variable.
// const关键字可以更改对象内部的属性
const OBJ = {
name:"yichuan",
age:18
}
console.log(OBJ);//{ name: 'yichuan', age: 18 }
OBJ.age = 20;
console.log(OBJ);//{ name: 'yichuan', age: 20 }
function func1(){
console.log(SUPERNUM);//ReferenceError: Cannot access 'SUPERNUM' before initialization
const SUPERNUM = 190;
}
func1();
function func2(){
const SUPERNUM = 190;
console.log(SUPERNUM);
const SUPERNUM = 200;
console.log(SUPERNUM);//SyntaxError: Identifier 'SUPERNUM' has already been declared
}
func2();
1.4 小结
var | let | const |
---|---|---|
存在变量提升 | 不存在变量提升 | 不存在变量提升 |
不需要初始化 | 不需要初始化 | 需要初始化赋值 |
函数作用域 | 块级作用域 | 块级作用域 |
可以重复声明 | 不可重复声明 | 不可重复声明 |
全局作用域下,变量会成为window的属性 | 全局作用域下,变量不会成为window的属性 | 全局作用域下,变量不会成为window的属性 |
2 数据类型
2.1 数据类型的划分
简单数据类型(原始数据类型)
Undefined
:只有一个值,特殊值undefined
,表示未定义Null
:只有一个值,特殊值null
,表示空值Boolean
:只有两个字面值,true
和false
,表示布尔值Number
:整数或浮点数两类数字,还有特殊值(-Infinity
、+Infinity
、NaN
),表示数值String
:表示0或多个16位Unicode
字符序列,用""或’’、``表示,表示字符串。Symbol
:一种符号实例唯一且不可改变的数据类型,表示为符号(ES6新增)
复杂数据类型(对象数据类型)
Object
:无名值对的集合,表示对象类型。Array
、Function
等都属于对象类型。
null和undefined的区别
在简单数据类型中,我们要着重注意理解null
和undefined
的区别。
null表示一个空指针对象,有值但是值为空值。所以在定义将来要保存对象值得变量时,建议使用null
来进行初始化,不要使用其他值。
undefined表示一个缺省值,即未定义值,此处本该有一个值,但是尚未定义。
String
String
数据类型表示0个或多个16位Unicode
字符序列,可以用""、’'以及反引号表示。我们可以使用length
获取字符串的字符长度,但是当字符串包含双字节字符时,那么length
返回的值可能不是准确的字符数。
为什么划分为简单数据类型和复杂数据类型?
在ES标准中存在着两种不同类型的数据:原始值和引用值。原始值──最简单的不可改变的数据,引用值──保存在内存中的对象。
不可变性和动态属性
不可变性指的是原始值本身是不可变的,动态属性指的是引用值可以动态增加、删除和修改属性和方法。分别举个栗子演示一下吧。
原始值:
let str = "yichuan";
str.substr(1,4);//"ichu"
str.slice(1,5);//"ichu"
str[5] = "zhenshuai";
console.log(str);//yichuan
我们看到,无论我们怎么操作str的字符串,都是在原字符串的基础上产生新的字符串,而非直接更改原字符串。
引用值:
let obj = {
name:"yichuan",
age:18
}
console.log(obj);//{name: "yichuan", age: 18}
obj.age = 20;
obj.gender = "male";
console.log(obj);//{name: "yichuan", age: 20, gender: "male"}
我们看到,对于对象obj可以随时随地进行增删改查操作属性和方法,可以进行动态改变。
对于原始值而言,其没有属性,只能使用原始字面量进行初始化。即使使用new关键字创建Object的字符串实例,其行为和方法也类似原始值。
let name = "yichuan";
let name2 = new String("yichuan2");
name.age = 18;
name2.age = 20;
console.log(name.age);//ubdefined
console.log(name2.age);//20
console.log(typeof name);//string
console.log(typeof name2);//object
2.2 值传递和引用传递
其实在内存空间根据存储形式分为栈内存和堆内存。
栈内存 | 堆内存 |
---|---|
存储的值大小固定 | 存储的值大小不定,可动态调整 |
空间较小 | 空间较大 |
可以直接操作其保存的变量,运行效率高 | 无法直接操作其内部存储,使用引用地址进行获取,运行效率低下 |
由系统自动分配的空间 | 通过代码进行分配空间 |
Javascript
就是根据值得存储位置分为简单数据类型和复杂数据类型的,也就是原始类型和引用类型。原始类型的值是存储在栈内存中的,在进行变量定义时系统自动分配内存空间;引用数据类型的引用地址存储在栈内存中,而真实的值存储在堆内存中,引用地址就是指向堆内存的。
我们看到,每定义一个变量赋值基本数据类型,都是在栈内存中开辟一块空间进行存储。而引用类型的值实际存储在堆内存中,它在栈中只存储了一个固定长度的地址,这个地址指向堆内存中的值。
所以当复制变量为基本数据类型时,复制的就是完完全全的变量和值;而复制变量为引用类型时,复制的只是值得引用地址。因此才会有深拷贝和浅拷贝一说。
2.3 数值转换
我们知道有三个函数能够将非数值类型转为数值类型,分别是:Number()
、parseInt()
以及parsetFloat()
,可适用于任何类型。字符串是不可变的,一旦创建,它的值不能变,要修改值的值必须销毁之前的字符串重新赋值。
toString
可以将当前值转换为字符串等价物,注意:null
和undefined
没有此toString
。String
和toString
的作用一样,但是它对null
和undefined
分别返回"null
“和”undefined
"。
Number函数
类型 | 值 |
---|---|
Boolean | ture转为1,false转为0 |
Number | 返回原数值即可 |
null | 0 |
undefined | NaN |
String | (1)对于字符串内的数值字符(无论什么进制、还是浮点型),会转换成对应Number类型的十进制数值。(2)对于空字符串转为0.(3)其他情况转为NaN |
对象类型 | (1)先使用valueOf()方法进行转换 (2)若转换得到的值为NaN,则调用toString()转换,而后按照String类型进行转换 |
parseInt函数
parseInt(string, radix)
解析一个字符串并返回指定基数的十进制整数, radix
是2-36
之间的整数,表示被解析字符串的基数。
-
要被解析的值。如果参数不是一个字符串,则将其转换为字符串(使用
ToString
抽象操作)。字符串开头的空白符将会被忽略。 -
radix
可选 从 2 到 36,表示字符串的基数。例如指定 16 表示被解析值是十六进制数。请注意,10不是默认值!
相比于Number()
方法而言,更加专注于对字符串中的数值进行操作。parseInt()
会从非空格字符开始进行转换。
- 当第一个字符不是数值字符、加号或减号,则立即返回NaN。(空字符串也返回NaN)
- 当第一个字符是数值字符或加号减号时,则依次挨个检测转换到字符串末尾,当碰到非数值字符则会忽略,而浮点字符也会转为只剩整数部分的值。如
123blue22.22
则转换为12322
。 - 当字符串中第一个字符是数值字符,且以
0x
等进制字符开头,则会转换为对应的十进制数。
parseFloat函数 和parseInt
函数工作类似,只不过检测是浮点型罢了。
2.4 常见类型转换
你应该知道的,Javascript
是一种弱类型的语言,因此在使用过程中会出现频繁的类型转换。而类型转换也会根据是否手动转换分为隐式转换和强制转换。
强制转换相对比较简单,无非就是我们自行进行转换,那么我们来介绍下隐式转换吧。
Boolean类型的等价隐式转换
我们知道if等判断、控制语句等会优先将其他类型值自动转为布尔值进行比较。
数据类型 | 转换为true的值 | 转换为false的值 |
---|---|---|
Boolean | true | false |
String | 非空字符串 | “”(空字符串) |
Number | 非0数值(包括无穷值) | 0、NaN |
Object | 任意对象(包括[],{}等) | null |
Undefined | N/A(不存在) | undefined |
运算符转换
我们经常遇到1+"yichuan"
和1-"yichuan"
等不同类型之间进行运算的情况,一般情况(-*/
)等会直接将非Number
类型转为Number
类型进行运算。你以为1+"1"
相加会等于2,其实得到却是11
,惊不惊喜,意不意外。
而在使用+
进行运算时,就需要格外注意。当进行+
运算时,有一侧是String
,那么则会将另一侧的数据类型转换为String
进行拼接;当+
运算的一侧Number
,而另一侧是引用类型时,则会将引用类型和Number
都转成String
类型后进行拼接;只有在+
一侧是Number
,另一侧非String
和引用类型时,才会将另一侧的类型转换为Number
后进行数值运算。
运算 | 值 |
---|---|
1 - true | 0 |
1 - null | 1 |
1 - “1” | 0 |
1 - []、1 - {} | 0 |
1 * undefined | NaN |
2 * [“1”] | 2 |
1 + “1” | 11 |
1 + null | 1 |
123 + {} | 123[object object] |
和=判断
===
和==
的区别,在于==
比较的两侧的值,当两侧的类型不同时会发生隐式转换;而===
比较的是两侧值和类型,当两者有一不同就得到false
,即不会发生隐式转换。对于===
就不探讨了,我们探讨下==
。
NaN涉及到NaN
的各种操作返回都是NaN
,NaN
也不等于任何值。也就是NaN == NaN
返回的也是false
。
Boolean和任何类型进行比较,都会优先将Boolean
转为Number
,但是在与undefined
和null
进行比较时,要考虑到两者均为假值的情况。因为undefined
和null
虽然会转为false
,但是false
是Boolean
类型会转为数值0所以不等。
比较 | 值 |
---|---|
true == 1 | true |
true == “2” | false |
true == [“1”] | true |
true == [“2”] | true |
true == [] | false |
undefined == false | false |
null == false | false |
String和Number在进行两者比较的时候,String
类型也会优先转换为Number
类型进行比较。
1 == "1" //true
0 == "" //true
null和undefined两者是假值,所以在比较的时候都会优先转换为false
进行比较。
比较 | 值 |
---|---|
null == undefined | true |
null == “” | false |
null == 0 | false |
null == false | false |
undefined == “” | false |
undefined == 0 | false |
undefines == false | false |
表中的转换前面都有介绍,null
和undefined
都会转换成false
进行比较,而其他则对应转换为numder
类型进行比较。
原始类型和引用类型
在两者进行比较时,有个需要注意的是引用类型会转换成原始类型进行比较,其实就是先转换成String
或Boolean
再进行比较。
`[object object]` == {} //true
`1,2,3` == [1,2,3] //true
而有个比较困扰的问题就是[] == ![]
的比较得到的为什么是true
?
[] == ![]
//1.首先要知道!的优先级高于==,因此![]先转换为false
//2.而![]转换为false后,会将false转换为数值0
//3.左侧的[]在==运算时,直接会转为数值0
//4.==两侧均为0,所以得到true
2.5 数据类型的判断
typeof判断
typeof
是Javascript原生内置的类型判断运算符,但是由于具有一定得局限性,在进行一些类型判断时并不能清晰准确。
示例:
function func(){
console.log("yichuan");
};
let obj = {
name: 'yichuan'
};
console.log(typeof undefined);//undefined
console.log(typeof true);//boolean
console.log(typeof 100);//number
console.log(typeof "yichuan");//string
console.log(typeof Symbol(1));//symbol
console.log(typeof null);//object
console.log(typeof [1,2,3]);//object
console.log(typeof func);//function
console.log(typeof obj);//object
console.log(typeof Math);//object
console.log(typeof new Date());//object
console.log(typeof /abc/ig);//object
console.log(typeof new Error("error"));//object
我们可以看到,typeof
可以判断所有基本数据类型,能够准确判断(number
、boolean
、string
、symbol
、undefined
),但是对于null
和object
类型(object
、function
、Error
等)却无能为力。
注意
typeof null
之所以返回值为object
,是因为在JS的最初版本使用的是32位系统,出于性能考虑使用了低位存储了变量的类型信息。000打头的表示对象,而null
也表示为全0,这就造成了误判其为object
类型,为了维持稳定也就一直持续到现在,所以这是js语言设计的一个缺陷。
instanceof判断
instanceof
操作符可以用于判断引用类型具体是什么类型的对象,通过内部机制的原型链查找去寻找对象的prototype
。但是其不能用于检测某些简单数据类型,因为它们没有原型怎么查找,所以也是有所缺陷的。
console.log([1,2,3] instanceof Array);//true
console.log([1,2,3] instanceof Object);//true
function func(){
console.log("yichuan");
}
console.log(func instanceof Function);//true
console.log(func instanceof Object);//true
console.log("yichuan" instanceof String);//false
console.log(new String("yichuan") instanceof String);//true
我们可以看到func和[1,2,3]使用instanceof
也都会指向Object
类型,其实instanceof
检测对象数据类型,也并不是很准确,同样会造成判断困扰。之所以会这样,这和原型链的查找机制相关,这里进行简单介绍,后面将单独写一篇文章进行介绍。(先挖一个坑)
原型链的几条基本准则:
- 所有复杂数据类型都有对象特性,可以进行自由进行属性操作
- 所有对象沿着原型链查找,查到顶部都是
null
空对象 - 所有复杂数据类型都具有一个
__proto__
(隐式原型)属性和prototype
(显式原型)属性,都是一个普通对象,其__proto__
值指向构造函数的protoype
- 当查找对象的属性时,会先对对象本身属性进行比较,如果对象本身没有此属性,则沿着原型链查找它的
__proto__
值
Object.prototype.toString.call()判断
我们看到前面的typeof
和instanceof
用于判断数据类型都具有局限性和缺陷,那么为了实现准确判断数据类型应该采用什么方法呢?答案是:Object.prototype.toString.call()
。
每一个对象数据类型都有toString
方法,也就是说toString()
方法会被每个Object
对象继承。如果此方法在自定义对象中未被覆盖,则toString()
返回 "[object type]
"的type
就是表示对象的类型。
我们看到自定义对象中未被覆盖被着重强调,意思就是告诉我们大部分对象数据类型重新书写了toString
方法,比如:Array
、Date
等。但是,实际生产中Object.prototype.toString
可以用于解决我们遇到的大部分类型判断问题,因此你可以放心使用。在使用时,我们还需要添加call
改变this
的指向。
Object.prototype.toString
原理是调用时取值内部的 [[Class]]
属性值,拼接成 '[object ' + [[Class]] + ']'
这样的字符串并返回. 然后我们使用 call
方法来获取任何值的数据类型.
方法调用 | 判断结果 |
---|---|
Object.prototype.toString.call(undefined) | [object Undefined] |
Object.prototype.toString.call(true) | [object Boolean] |
Object.prototype.toString.call(null) | [object Null] |
Object.prototype.toString.call("yichuan") | [object String] |
Object.prototype.toString.call(100) | [object Number] |
Object.prototype.toString.call(Symbol(100)) | [object Symbol] |
Object.prototype.toString.call({}) | [object Object] |
Object.prototype.toString.call([]) | [object Array] |
Object.prototype.toString.call(new Error()) | [object Error] |
Object.prototype.toString.call(new Date()) | [object Date] |
Object.prototype.toString.call(function(){}) | [object Function] |
Object.prototype.toString.call(/123/gi) | [object RegExp] |
Object.prototype.toString.call(Math) | [object Math] |
Object.prototype.toString.call(JSON) | [object JSON] |
Object.prototype.toString.call(window) | [object Window] |
集大成者──JQuery中的类型判断
JQuery
的类型判断源码,其实就是使用typeof
判断简单数据类型,使用Object.prototype.toString.call()
来判断复杂数据类型,使用class2type
截取取到数据类型的名称。快看这,这是源码哦,写的很简单,并没有大家想象的那么复杂。
var class2type = {};
jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ),
function( i, name ) {
class2type[ "[object " + name + "]" ] = name.toLowerCase();
} );
type: function( obj ) {
if ( obj == null ) {
return obj + "";
}
return typeof obj === "object" || typeof obj === "function" ?
class2type[Object.prototype.toString.call(obj) ] || "object" :
typeof obj;
}
isFunction: function( obj ) {
return jQuery.type(obj) === "function";
}
参考文章
《JavaScript高级程序设计(第4版)》
写在最后
感谢大家的阅读,我将继续和大家分享更多优秀的文章,此文参考了大量书籍和文章,如果有错误和纰漏,希望能给予指正。
关注微信公众号【前端万有引力】,及时获取更多相关技术、面试经验等文章。