语言基础:变量声明、数据类型

178 阅读16分钟

变量

ECMAScript变量是松散类型的,意思是变量可以用于保存任何类型的数据。每个变量只不过是一个用于保存任意值的命名占位符。

声明变量的方式:var、let、const

照惯例,先放总结,再细细讲特点:

作用域分为:全局作用域、函数作用域、块作用域三种。

声明方式变量作用域其他特点
var a = 100最外层声明时,作用域为全局作用域;在函数内声明时,作用域为函数作用域,且随着函数内存释放而消失。变量提升,多次使用var声明同一个变量都没问题
leta = 100块作用域,即使在最外层声明时,作用域也不是全局作用域。暂时性死区,在一个作用域内多次使用let声明同一个变量会报错
consta = 100块作用域,即使在最外层声明时,作用域也不是全局作用域。声明变量时必须同时初始化变量,后续尝试修改const声明的变量会导致运行时错误
a = 100全局作用域,不管在哪里声明,都是全局变量,挂载在window上。-

var

声明方式有多种,分别是声明但不赋值、声明且赋值、在一条语句中用逗号分隔每个变量以实现同时声明多个变量:

  • var message
  • var message= "hi"
  • var message = "hi", found = false, age = 29;

有一种非常规的声明方式var a=b=100,语句执行顺序为var a→b=100→a=b,最终结果是a=100,b=100

声明作用域:函数作用域

下文的代码块中在最外层以及函数体内都声明了message:

  • 最外层的message作用域是全局作用域,声明之后直接绑定在window上
  • 函数体内的message的作用域是函数作用域。
var message = "hello"
function test() {
    console.log(message)//undefined
    var message = "hi";
    console.log(message)//hi
}
test();
console.log(message) // hello;
console.log(window.message) //hello;
变量提升

变量提升:把所有变量声明都拉到函数作用域的顶部。

使用var时,下面代码不会报错。这是因为var关键字声明的变量被自动提升到函数作用域顶部:

function foo() {
    console.log(age);
    var age = 26;
}
foo(); // undefined

在ECMAScript运行时中,上面的代码等价于下面的代码:

function foo() {
    var age;
    console.log(age);
    age = 26;
}
foo(); //undefined

let

let跟var的作用差不多,但有着非常重要的区别,以下的let特点基本都会和var来对比展示。

声明作用域:块作用域

let 声明的范围是块作用域,而var声明的范围是函数作用域。

if(true) {
    var name = 'Matt'
    console.log(name) // Matt
}
console.log(name) // Matt
-----------------------
if(true) {
    let age = 26
    console.log(age) // 26
}
console.log(age) // ReferenceError: age没有定义

age变量的作用域仅限于if块内部,所以不能在if块外部被引用。块作用域是函数作用域的子集,因此适用于var的作用域限制同样也适用于let。

不允许同一个块作用域中出现冗余声明
var name
var name
------------------------
let age
let age // SyntaxError: 标识符age已经声明过了
------------------------
var name
let name //SyntaxError
------------------------
let age
var age //SyntaxError
暂时性死区:变量不会在作用域中被提升
// name会被提升
console.log(name) // undefined
var name = 'Matt'

// age不会被提升
console.log(age) // ReferenceError: age 没有定义
let age = 26
全局声明的变量不会成为window对象的属性
var name = 'Matt'
console.log(window.name) // 'Matt'

let age = 26
console.log(window.age) // undefined
for循环中的let声明不会渗透到循环体外部
for (var i = 0; i < 5; i++) {...}
console.log(i) // 5

for (let i = 0;; i < 5; i++) {...}
console.log(i) // ReferenceError: i 没有定义
------------------------------------
for (var i = 0; i < 5; ++i) {
    setTimeout(() => console.log(i), 0) // 5,5,5,5,5
}

for (let i = 0; i < 5; ++i) {
    setTimeout(() => console.log(i), 0) // 0,1,2,3,4
}
  • var声明的for循环中,i值已经声明在块作用域之外,每次循环都会修改i的值,等到异步操作开始执行时,for循环已经结束,输出的值就是i的最终结果5
  • let声明的for循环中,每次迭代都会声明一个独立变量实例,与此同时,let声明的迭代变量也会进行自增,所以5次setTimeout输出的i其实是5个独立变量实例,也就是5个不同的值,所以console.log输出的是我们期望的值,也就是循环执行过程中每个迭代变量的值。

这种每次迭代声明一个独立变量实例的行为适用于所有风格的for循环,包括for-in和for-of。

const声明

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

const age = 26
age = 36 // TypeError: 给常量赋值

// const 不允许重复声明
const name = 'Matt'
const name = 'Nicholas' // SyntaxError

// const 声明的作用域也是块
const name = 'Matt'
if (true) { const name = 'Nicholas' }
console.log(name)
const声明的限制只适用于它指向的变量的引用

如果 const 变量引用的是一个对象,那么修改这个对象内部的属性并不违反const的限制,如下所示:

const person = {}
person.name = 'Matt' // ok
不能用const来声明迭代变量,因为迭代变量会自增
for(const i = 0; i < 10; ++i) {} // TypeError: 给常量赋值

不过,如果你使用const来声明一个实体数组或者实体对象的一个不会被修改的for循环变量时是可以的,每次迭代只是创建一个新变量,而不是修改迭代变量。如下:

const obj = {a: 1, b: 2}
for (const key in obj) { console.log(key) } // a,b

const list = [1,2,3,4,5]
for (const value of list) { console.log(value) } // 1,2,3,4,5

变量声明总结:优先使用const、必须声明变量时采用let,弃用var

  • let和const有了明确的作用域、声明位置,甚至不变的值,使用这两种声明方式,有助于提升代码质量
  • 使用const声明可以让浏览器运行时强制保持变量不变,也可以让静态代码分析工具提前发现不合法的赋值操作。

数据类型

ECMAScript有7种数据类型。

  • 原始类型:Undefined、Null、Bollean、Number、String和Symbol
  • 复杂数据类型:Object

类型确定器:typeof 操作符

为什么需要typeof,typeof有什么用?

因为ECMAScript的类型系统是松散的,所以需要一种手段来确定任意变量的数据类型。这就是typeof诞生的理由。

对一个值使用typeof操作符会返回下列字符串之一:

  • “undefined”表示值未定义
  • “boolean”表示值为布尔值
  • “string”表示值为字符串
  • “number”表示值为数值
  • “object”表示值为对象或null(调用typeof null返回的是"object",这是因为特殊值null被认为是一个对空对象的引用)
  • “function”表示值为函数
  • “symbol”表示值为符号

下面是使用typeof操作符的例子:

let message = "some string"
console.log(typeof message) // "string"
console.log(typeof (message)) // "string"
console.log(typeof 95) // "number"

严格来讲,函数也被认为是对象,并不代表一种数据类型。可是函数也有自己特殊的属性。为此,就有必要通过typeof操作符来区分函数和其他对象。

Undefined类型

  • Undefined类型只有一个值,就是特殊值undefined
  • 包含undefined值的变量跟未定义变量是有区别的
  • 字面量undefined主要用于比较,增加这个特殊值的目的就是为了明确空指针对象(null)和未初始化变量的区别
  • 当使用var或let声明了变量但没有初始化变量值,就相当于给变量赋予了undefined
let message // 等同于 let message = undefined
console.log(message == undefined) // true

undefined与typeof

无论是声明后未初始化还是未声明的变量,返回的结果都是“undefined”,如下:

let message // 这个变量被声明了,只是值为undefined
// let age

console.log(typeof message) // “undefined”
console.log(typeof age) // “undefined”

undefined与boolean

let message 
// let age

if (message) {...} // 这个块不会被执行

if (!message) {...} // 这个块会执行

if (age) {...} // 这里会报错

Null 类型

  • Null类型只有一个值,即特殊值null,逻辑上讲,null值表示一个空对象指针,这也是给typeof穿一个null会返回“object”的原因
  • 在定义将来要保存对象值的变量时,建议使用null来初始化,不要使用其他值。这样,只要检查这个变量的值是不是null就可以知道这个变量是否在后来被重新赋予了一个对象的引用
let car = null
console.log(typeof car) // "object"
if (car != null) {// car是一个对象的引用}

null与undefined

  • undefined值是由null值派生而来的,因此ECMA-262将它们定义为表面上相等
  • 用等于操作符(==)比较null和undefined始终返回true。这个操作符会为了比较而转换它的操作数
console.log(null == undefined) // true

null的用途

只要变量要保存对象,而当新建变量时,要保存的对象值还未存在的话,就要用null来填充该变量。这样就可以保持null是空对象指针的语义,并进一步将其与undefined区分开来。

null与boolean

let message = null
let age

if (message) {...} // 这个块不会执行
if (!message) {...} // 这个块会执行

if (age) {...} // 这个块不会执行
if (!age) {...} // 这个块会执行

Boolean类型

Boolean有两个字面量:true和false

其他类型的值使用Boolean()转型函数转为Boolean类型

let message = "Hello world!"
let messageAsBoolean = Boolean(message) // true

不同类型与布尔值之间的转换规则

在if等流控制语句会自动执行其他类型值到布尔值的转换。

数据类型转换为true的值转换为false的值
Booleantruefalse
String非空字符串空字符串
Number非零数值(包括无穷值)0、NaN
Object任意对象null
UndefinedN/A(不存在)undefined

Number类型

整数

进制类型例子声明关键
十进制let intNum = 55-
八进制let octalNum = 070 // 八进制的56第一个数字必须是0,后续是八进制数字(0~7),如果字面量中包含的数字超出了应有的范围,就会忽略前缀的0
十六进制let hexNum = 0xA前缀是 0x(区分大小写),然后是十六进制数字(09、AF)。十六进制中的字母大小写均可

八进制和十六进制格式创建的数值在所有数学操作中都被视为十进制数值。

let hexNum1 = 0xA // 10
let hexNum2 = 0x1f // 31

console.log(hexNum1 + hexNum2) // 41

浮点值

浮点值的特点:

  • 要定义浮点值,数值中必须包含小数点,而且小数点后面必须至少有一个数字 // 例如 1.1 / 0.1
  • 存储浮点值使用的内存空间是存储整数值的两倍
  • 在小数点后面没有数字,或者小数点后面的数为0,那它会被转换为整数保存 // 例如1. / 1.0
  • 浮点值的精确度不可靠,浮点值要进行计算时,应该先乘上对应的倍率转成整数,计算完成后再转回浮点值
科学计数法
  • 对于非常大或非常小的数值,浮点值可以用科学计数法来表示
  • ECMAScript中科学计数法的格式要求是一个数值后跟一个e(大小写都行),再加上一个要乘的10的多少次幂。例如let floatNum = 3.125e7等于 31250000
  • 默认情况下,ECMAScript会将小数点后至少包含6个零的浮点值转换为科学计数法

Infinity值:无限大

  • 由于内存的限制,Number有最大值和最小值,可以通过调用Number.MAX_VALUE和Number.MIN_VALUE来确定
  • 如果某个计算得到的数值结果超出了JavaScript可以表示的范围,那么这个数值会被自动转换成Infinity或 -Infinity
  • Infinity数值无法进行计算
  • 要确定一个值是不是有限大,可以通过isFinite(num)来判断
  • 如果分子是非0值,分母是0或-0,都会返回Infinity或-Infinity。例如:5/05/-0

NaN:不是数值(Not a Number)

  • 表示本来要返回数值的操作失败了(而不是抛出错误)。比如0/0
  • 任何涉及到NaN的操作始终返回NaN(如NaN/10)
  • NaN不等于包括NaN在内的任何值。所以NaN == NaN会返回false
isNaN(value):判断value是否“不是数值”,任何不能转换为数值的值都会返回true
console.log(isNaN(NaN)); // true 
console.log(isNaN(10)); // false,10是数值
console.log(isNaN("10")); // false,可以转换为数值10 
console.log(isNaN("blue")); // true,不可以转换为数值
console.log(isNaN(true)); // false,可以转换为数值1

转换成数值的方法有三个:Number()、parseInt()和parseFloat()

  • Number是转型函数,可用于任何数据类型
  • parseInt与parseFloat主要用于将字符串转换成数值
Number()函数的转换规则
  • 布尔值,true转为1,false转为0
  • 数值,直接返回
  • numm,返回0
  • undefined,返回NaN
  • 字符串比较复杂,规则如下:
    • 如果字符串包含数值字符,包括数值字符前面带+、-号的情况,则转换为一个十进制数值。因此Number("1")返回1,Number("-100")返回-100,Number("011")返回11(忽略前面的0)
    • 如果字符串包含有效的浮点值,则会转换为相应的浮点值,规则参照上一条
    • 如果字符串包含有效的十六进制格式如“0xf”,则会转换为与该十六进制对应的十进制整数值
    • 如果是空字符串(不包含字符),则返回0
    • 如果字符串包含除上述情况之外的其他字符,则返回NaN
  • 对象,调用valueOf()方法,并按照上述规则转换返回的值。如果转换结果是NaN,则调用toString()方法,再按照转换字符串的规则转换。
  • 数组,空数组返回0,只有一位数据的数组,将按照上述规则转换返回首位的值,拥有多位数据的数组,返回NaN
优先选择使用parseInt()方法来转换数值
  • parseInt()函数更专注于字符串是否包含数值模式。
  • 字符串最前面的空格会被忽略,从第一个非空格字符串开始转换。
  • 如果第一个字符不是数值字符、加号或减号,parseInt()立即返回NaN。这意味着空字符串也会返回NaN。
  • 如果第一个字符是数值字符、加号或减号,则依次检测每个字符,直到字符串末尾,或碰到非数值字符。比如,“1234blue”会转换为1234,“22.5”会转换为22。
  • 如果字符串以“0x”开头,就会被解释为十六进制整数。如果字符串以“0”开头,且紧跟着数值字符,在非严格模式下会被某些实现解释为八进制整数。
parseInt的转换参数示例
let num1 = parseInt("1234blue") //1234
let num2 = parseInt("") // NaN
let num3 = parseInt("0xA") // 10,解释为十六进制整数
let num4 = parseInt(22.5) // 22
let num5 = parseInt("60") // 60
let num6 = parseInt(“0xf”) // 15,解释为十六进制整数
------------------------------------
let num1 = parseInt("10", 2) // 2,按二进制解析
let num2 = parseInt("10", 8) // 8,按八进制解析
let num3 = parseInt("10", 10) // 10,按十进制解析
let num4 = parseInt("10", 16// 16,按十六进制解析

String 类型

字符串可以使用双引号(“”)、单引号(‘’)或反引号(`)标示

toString()方法,返回除了null和undefined之外的当前值的字符串等价物

let age = 11
let ageAsString = age.toString() // 字符串“11”
let found = true
let foundAsString = found.toString() // 字符串“true

toString()方法可见于数值、布尔值、对象和字符串值。null和undefined值没有toString()方法。

如果一个值不确定是不是null或undefined,可以使用String()转型函数,它始终会返回表示相应类型值的字符串,String()函数遵循如下规则:

  • 如果值有toString()方法,则调用该方法并返回结果
  • 如果值是null,则返回“null”
  • 如果值是undefined,则返回“undefined”

Symbol 类型,创建唯一记号的符号类型

Symbol的特点:

  • 数据类型是原始类型
  • 符号实例是唯一、不可变的
  • 符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。

用Symbol()函数创建符号实例,而非new Symbol构造函数

创建示例:

let sym = Symbol()
console.log(typeof sym) // symbol

Symbol函数可以接收一个字符串参数,函数返回的实例和参数并无关系,但是可以通过打印实例来输出参数变量,如下所示:

let Symbol = Symbol()
let otherSymbol = Symbol()

console.log(Symbol) // Symbol()
console.log(Symbol == otherSymbol) // false

let fooSymbol = Symbol('foo')
let otherfooSymbol = Symbol('foo')

console.log(fooSymbol) // Symbol('foo')
console.log(fooSymbol == otherfooSymbol) // false

用构造函数无法创建Symbol对象,只会报错

let myBoolean = new Boolean()
console.log(typeof myBoolean) // object

let myString = new String()
console.log(typeof myString) // object

let myNumber = new Number()
console.log(typeof myNumber) // object

let mySymbol = new Symbol() // TypeError: Symbol is not a constructor

如果你确实想用符号包装对象,可以借用Object()函数:

let mySymbol = Symbol()
let myWrappedSymbol = Object(mySymbol)
console.log(typeof myWrappedSymbol) // object

使用Symbol.for()方法声明全局符号注册表,让符号实例可以被共享和重用

用法如下:

let symbol = Symbol.for('foo')
let otherSymbol = Symbol.for('foo')
console.log(typeof Symbol) // symbol
console.log(otherSymbol === symbol) // true

值得注意的是,即使采用相同的符号描述,在全局注册表中定义的符号跟使用Symbol()定义的符号也并不等同:

let symbol = Symbol('foo')
let otherSymbol = Symbol.foo('foo')

console.log(symbol == otherSymbol) // false

使用Symbol.keyFor(),通过接收一个符号参数来查询全局注册表对应的字符串键

// 创建全局符号
let s = Symbol.for('foo')
console.log(Symbol.keyFor(s)) // foo

// 创建普通符号
let s2 = Symbol('bar')
console.log(Symbol.keyFor(s2)) // undefined

Symbol属性的使用

使用方法如下:

let s1 = Symbol()
let s2 = Symbol()

let o = {[s1]: 'foo val'}
------------------------
o[s1] = 'foo val'
------------------------
Object.defineProperty(o, s2, {value: 'bar val'}
------------------------
Object.defineProperties(o, {
    [s1]: {value: 'baz val'},
    [s2]: {value: 'qux val'}
})

Object类型,一组数据和功能的集合

可以通过new Object()来创建对象

let o = new Object()
--------------------
let o = new Object // 合法,但不推荐

o[key] = value

Object.defineProperty(o,key,{value: 'value'}

Object.defineProperties(o, {
    [key1]: {value: 'value1'},
    [key2]: {value: 'value2'}
})

每个Object实例都有如下属性和方法:

  • constructor:用于创建当前对象的函数。
  • hasOwnProperty(propertyName):用于判断当前对象实例(而非原型链)上是否存在给定的属性。要检查的属性名必须是字符串(如o.hasOwnProperty("name"))或符号。
  • isPrototypeOf(object):用于判断当前对象是否为另一个对象的原型。
  • PropertyIsEnumerable(propertyName):用于判断给定的属性是否可以使用for-in进行枚举。与hasOwnProperty()一样,属性名必须是字符串。
  • toLocaleString():返回对象的本地化后的字符串。
  • toString():返回以字符串形式返回该对象。
  • valueOf():返回对象对应的字符串、数值或布尔值表示。通常与toString()的返回值相同。