【前端】你真的理解JavaScript中的变量和数据类型吗

前言

在我们学习各种编程语言时,最先接触的就是变量和数据类型,那么我们真的理记住和理解了这些最基础的知识点吗?我们是否在笔试和面试的时候,经常对一些不起眼的知识点想不起来?那么跟着这篇文章重新回顾下这些基础内容吧。

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 小结

varletconst
存在变量提升不存在变量提升不存在变量提升
不需要初始化不需要初始化需要初始化赋值
函数作用域块级作用域块级作用域
可以重复声明不可重复声明不可重复声明
全局作用域下,变量会成为window的属性全局作用域下,变量不会成为window的属性全局作用域下,变量不会成为window的属性

2 数据类型

2.1 数据类型的划分

简单数据类型(原始数据类型)

  • Undefined:只有一个值,特殊值undefined,表示未定义
  • Null:只有一个值,特殊值null,表示空值
  • Boolean:只有两个字面值,truefalse,表示布尔值
  • Number:整数或浮点数两类数字,还有特殊值(-Infinity+InfinityNaN),表示数值
  • String:表示0或多个16位Unicode字符序列,用""或’’、``表示,表示字符串。
  • Symbol:一种符号实例唯一且不可改变的数据类型,表示为符号(ES6新增)

复杂数据类型(对象数据类型)

  • Object:无名值对的集合,表示对象类型。ArrayFunction等都属于对象类型。
null和undefined的区别

在简单数据类型中,我们要着重注意理解nullundefined的区别。

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可以将当前值转换为字符串等价物,注意:nullundefined没有此toString
  • StringtoString的作用一样,但是它对nullundefined分别返回"null“和”undefined"。

Number函数

类型
Booleanture转为1,false转为0
Number返回原数值即可
null0
undefinedNaN
String(1)对于字符串内的数值字符(无论什么进制、还是浮点型),会转换成对应Number类型的十进制数值。(2)对于空字符串转为0.(3)其他情况转为NaN
对象类型(1)先使用valueOf()方法进行转换 (2)若转换得到的值为NaN,则调用toString()转换,而后按照String类型进行转换

parseInt函数
parseInt(string, radix) 解析一个字符串并返回指定基数的十进制整数, radix2-36之间的整数,表示被解析字符串的基数。

  • 要被解析的值。如果参数不是一个字符串,则将其转换为字符串(使用 ToString 抽象操作)。字符串开头的空白符将会被忽略。

  • radix 可选 从 2 到 36,表示字符串的基数。例如指定 16 表示被解析值是十六进制数。请注意,10不是默认值!

相比于Number()方法而言,更加专注于对字符串中的数值进行操作。parseInt()会从非空格字符开始进行转换。

  • 当第一个字符不是数值字符、加号或减号,则立即返回NaN。(空字符串也返回NaN)
  • 当第一个字符是数值字符或加号减号时,则依次挨个检测转换到字符串末尾,当碰到非数值字符则会忽略,而浮点字符也会转为只剩整数部分的值。如123blue22.22则转换为12322
  • 当字符串中第一个字符是数值字符,且以0x等进制字符开头,则会转换为对应的十进制数。

parseFloat函数parseInt函数工作类似,只不过检测是浮点型罢了。

2.4 常见类型转换

你应该知道的,Javascript是一种弱类型的语言,因此在使用过程中会出现频繁的类型转换。而类型转换也会根据是否手动转换分为隐式转换强制转换

强制转换相对比较简单,无非就是我们自行进行转换,那么我们来介绍下隐式转换吧。

Boolean类型的等价隐式转换

我们知道if等判断、控制语句等会优先将其他类型值自动转为布尔值进行比较。

数据类型转换为true的值转换为false的值
Booleantruefalse
String非空字符串“”(空字符串)
Number非0数值(包括无穷值)0、NaN
Object任意对象(包括[],{}等)null
UndefinedN/A(不存在)undefined
运算符转换

我们经常遇到1+"yichuan"1-"yichuan"等不同类型之间进行运算的情况,一般情况(-*/)等会直接将非Number类型转为Number类型进行运算。你以为1+"1"相加会等于2,其实得到却是11,惊不惊喜,意不意外。

而在使用+进行运算时,就需要格外注意。当进行+运算时,有一侧是String,那么则会将另一侧的数据类型转换为String进行拼接;当+运算的一侧Number,而另一侧是引用类型时,则会将引用类型和Number都转成String类型后进行拼接;只有在+一侧是Number,另一侧非String和引用类型时,才会将另一侧的类型转换为Number后进行数值运算。

运算
1 - true0
1 - null1
1 - “1”0
1 - []、1 - {}0
1 * undefinedNaN
2 * [“1”]2
1 + “1”11
1 + null1
123 + {}123[object object]
=判断

=====的区别,在于==比较的两侧的值,当两侧的类型不同时会发生隐式转换;而===比较的是两侧值和类型,当两者有一不同就得到false,即不会发生隐式转换。对于===就不探讨了,我们探讨下==

NaN涉及到NaN的各种操作返回都是NaNNaN也不等于任何值。也就是NaN == NaN返回的也是false

Boolean和任何类型进行比较,都会优先将Boolean转为Number,但是在与undefinednull进行比较时,要考虑到两者均为假值的情况。因为undefinednull虽然会转为false,但是falseBoolean类型会转为数值0所以不等。

比较
true == 1true
true == “2”false
true == [“1”]true
true == [“2”]true
true == []false
undefined == falsefalse
null == falsefalse

String和Number在进行两者比较的时候,String类型也会优先转换为Number类型进行比较。

1 == "1" //true
0 == "" //true

null和undefined两者是假值,所以在比较的时候都会优先转换为false进行比较。

比较
null == undefinedtrue
null == “”false
null == 0false
null == falsefalse
undefined == “”false
undefined == 0false
undefines == falsefalse

表中的转换前面都有介绍,nullundefined都会转换成false进行比较,而其他则对应转换为numder类型进行比较。

原始类型和引用类型
在两者进行比较时,有个需要注意的是引用类型会转换成原始类型进行比较,其实就是先转换成StringBoolean再进行比较。

`[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可以判断所有基本数据类型,能够准确判断(numberbooleanstringsymbolundefined),但是对于nullobject类型(objectfunctionError等)却无能为力。

注意
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()判断

我们看到前面的typeofinstanceof用于判断数据类型都具有局限性和缺陷,那么为了实现准确判断数据类型应该采用什么方法呢?答案是:Object.prototype.toString.call()

每一个对象数据类型都有toString方法,也就是说toString()方法会被每个Object对象继承。如果此方法在自定义对象中未被覆盖,则toString()返回 "[object type]"的type就是表示对象的类型。

我们看到自定义对象中未被覆盖被着重强调,意思就是告诉我们大部分对象数据类型重新书写了toString方法,比如:ArrayDate等。但是,实际生产中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版)》

《一看就懂的var、let、const三者区别》

《你真的掌握变量和类型了吗》

《Javascript 中的数据类型判断》

写在最后

感谢大家的阅读,我将继续和大家分享更多优秀的文章,此文参考了大量书籍和文章,如果有错误和纰漏,希望能给予指正。

关注微信公众号【前端万有引力】,及时获取更多相关技术、面试经验等文章。
在这里插入图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值