# 变量与数据类型

请使用 var 运算符声明变量。

# 标识符命名规范

# 命名规范

  1. 严格区分大小写;
  • 命名必须以字母或_$开头,余下的部分可以是任意的字母,数字,或者是_或者是$
  • 不能用关键字或者是保留字命名;
  • javascript 自己的命名习惯;
    • 驼峰命名法:getElementById
    • 首字母大写:Object
  • 命名要有意义。

# 关键字与保留字

  • ECMA-262 定义了 ECMAScript 支持的一套关键字(keyword)、保留字(reserved word)。
  • 关键字标识了 ECMAScript 语句的开头和/或结尾。根据规定,关键字是保留的,不能用作变量名或函数名。
  • 保留字在某种意思上是为将来的关键字而保留的单词。因此保留字不能被用作变量名或函数名。
关键字保留字
abstract arguments boolean break byte case
catch char class const continue debugger
default delete do double else enum
eval export extends false final finally
float for function goto if implements
import in instanceof int interface let
long native new null package private
protected public return short static super
switch synchronized this throws transient true
try typeof var void volatile while
with yield

WARNING

如果将保留字用作变量名或函数名,那么除非将来的浏览器实现了该保留字,否则很可能收不到任何错误消息。当浏览器将其实现后,该单词将被看做关键字,如此将出现关键字错误。

var for = 123;
//Uncaught SyntaxError: Unexpected token for

# 变量

# 变量是什么?

WARNING

变量,就是一个用于存放数值的容器。

var name = "张三";
alert("hello "+name); // hello 张三

var num = 100;
alert(num*10);        // 1000

TIP

变量是用来存储数值的。变量不是数值本身,它们仅仅是一个用于存储数值的容器。 你可以把变量想象成一个个用来装东西的纸箱子。

存储数据
  • 变量的独特之处在于它存放的数值是可以改变的。
  • 变量它们能够存储任何的东西 -- 不只是字符串和数字。变量可以存储更复杂的数据,甚至是函数。

# 声明变量

JavaScript 有三种声明方式。

关键词 描述 版本
var 声明一个变量,可赋一个初始化值。 ES5
let 声明一个块作用域的局部变量,可赋一个初始化值。 ES6
const 声明一个块作用域的只读的命名常量。 ES6

# 基本语法

声明一个变量的语法是在 var 关键字之后加上这个变量的名字:

var myName;
var myAge;

变量声明后,即可使用,以下通过输出变量方式来使用:

console.log(myName);   //undefined
console.log(myAge);    //undefined

以上这两个变量并没有赋值,他们是空的容器(在 Javascript 中用 undefined 表示,未定义)。

同时声明多个变量,通过,隔开:

var myName,myAge,mySex;

TIP

提示: 千万不要把两个概念弄混淆了,“一个变量存在,但是没有赋值”和“一个变量并不存在” — 他们完全是两回事.

//声明未赋值
var myName;
console.log(myName);     //输出undefined

//未声明
console.log(firstName);  //报错 firstName is not defined

WARNING

用 var 或 let 语句声明的变量,如果没有赋初始值,则其值为 undefined。

# 初始化变量

初始化变量有两种方式:

  1. 先声明再初始化
  2. 声明变量并初始化

先声明再初始化,方法如下,在变量名之后跟上一个“=”,然后是值:

var myName;
var myAge;

myName="Nico";
myAge = 19;

console.log(myName);  //输出 Nico
console.log(myAge);   //输出 19

【重点】声明变量并初始化:

var myName = "Nico";
var myAge = 19;

console.log(myName);  //输出 Nico
console.log(myAge);   //输出 19

# 更新变量

一旦变量赋值,您可以通过简单地给它一个不同的值来更新它。

var myName = 'Nico';
console.log(myName); //输出 Nico

myName = "Allcky";
console.log(myName); //输出 Allcky

# [es6]常量(Constants)

你可以用关键字 const 创建一个只读的常量。

const PI = 3.141592654 ;
console.log(PI); //输出 3.141592654
  • 常量不可以通过赋值改变其值,也不可以在脚本运行时重新声明。它必须被初始化为某个值。
  • 常量的作用域规则与 let 块级作用域变量相同。若省略 const 关键字,则该标识符将被视为变量。
  • 在同一作用域中,不能使用与变量名或函数名相同的名字来命名常量。

不能使用与变量名或函数名相同的名字来命名常量,例如:

function f() {};
const f = 5;
//Uncaught SyntaxError: Identifier 'f' has already been declared
//语法错误: 标识符 'f' 已经被声明

常量标识符的命名规则和变量相同:必须以字母、下划线或美元符号开头并可以包含有字母、数字或下划线。

常量对象的属性是不受保护的,所以可以修改常量对象的属性,例如:

const MY_OBJECT = {"key": "value"};
MY_OBJECT.key = "otherValue";

利用 ES6 中对象的方法 Object.freeze()可将对象冻结,从而无法修改其属性 (详见Object对象一节)

# 变量声明兼容性

letconst是 ES6 新增声明方式,其存在兼容问题:

# let const var 区别

# var

  1. var 声明作用域 关键的问题在于,使用 var 操作符定义的变量会成为包含它的函数的局部变量。比如,使用 var 在一个函数内部定义一个变量,就意味着该变量将在函数退出时被销毁:
function test() { 
 var message = "hi"; // 局部变量
} 
test(); 
console.log(message); // 出错!

这里,message 变量是在函数内部使用 var 定义的。函数叫 test(),调用它会创建这个变量并给 它赋值。调用之后变量随即被销毁,因此示例中的最后一行会导致错误。不过,在函数内定义变量时省 略 var 操作符,可以创建一个全局变量:

function test() { 
 message = "hi"; // 全局变量
} 
test(); 
console.log(message); // "hi"

去掉之前的 var 操作符之后,message 就变成了全局变量。只要调用一次函数 test(),就会定义 这个变量,并且可以在函数外部访问到。

  1. var 声明提升 使用 var 时,下面的代码不会报错。这是因为使用这个关键字声明的变量会自动提升到函数作用域 顶部:
function foo() { 
 console.log(age); 
 var age = 26; 
} 
foo(); // undefined 
// 之所以不会报错,是因为 ECMAScript 运行时把它看成等价于如下代码:
function foo() { 
 var age; 
 console.log(age); 
 age = 26; 
} 
foo(); // undefined 
function foo() { 
 var age = 16; 
 var age = 26; 
 var age = 36; 
 console.log(age); 
} 
foo(); // 36

# let

let 跟 var 的作用差不多,但有着非常重要的区别。最明显的区别是,let 声明的范围是块作用域, 而 var 声明的范围是函数作用域。

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

// 重复声明
var name; 
var name; 

let age; 
let age; // SyntaxError;标识符 age 已经声明过了

JavaScript 引擎会记录用于变量声明的标识符及其所在的块作用域,因此嵌套使用相同的标 识符不会报错,而这是因为同一个块中没有重复声明:

var name = 'Nicholas'; 
console.log(name); // 'Nicholas' 
if (true) { 
 var name = 'Matt'; 
 console.log(name); // 'Matt' 
} 
let age = 30; 
console.log(age); // 30 
if (true) { 
 let age = 26; 
 console.log(age); // 26 
} 

对声明冗余报错不会因混用 let 和 var 而受影响。这两个关键字声明的并不是不同类型的变量, 它们只是指出变量在相关作用域如何存在。

var name; 
let name; // SyntaxError 
let age; 
var age; // SyntaxError

暂时性死区 let 与 var 的另一个重要的区别,就是 let 声明的变量不会在作用域中被提升。

// name 会被提升
console.log(name); // undefined 
var name = 'Matt'; 
// age 不会被提升
console.log(age); // ReferenceError:age 没有定义
let age = 26; 

在解析代码时,JavaScript 引擎也会注意出现在块后面的 let 声明,只不过在此之前不能以任何方 式来引用未声明的变量。在 let 声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此 阶段引用任何后面才声明的变量都会抛出 ReferenceError。

全局声明 与 var 关键字不同,使用 let 在全局作用域中声明的变量不会成为 window 对象的属性(var 声 明的变量则会)。

条件声明 在使用 var 声明变量时,由于声明会被提升,JavaScript 引擎会自动将多余的声明在作用域顶部合 并为一个声明。因为 let 的作用域是块,所以不可能检查前面是否已经使用 let 声明过同名变量,同 时也就不可能在没有声明的情况下声明它。

<script> 
 var name = 'Nicholas'; 
 let age = 26; 
</script> 
<script> 
 // 假设脚本不确定页面中是否已经声明了同名变量
 // 那它可以假设还没有声明过
 var name = 'Matt'; 
 // 这里没问题,因为可以被作为一个提升声明来处理
 // 不需要检查之前是否声明过同名变量
 let age = 36; 
 // 如果 age 之前声明过,这里会报错
</script> 
// 使用 try/catch 语句或 typeof 操作符也不能解决,因为条件块中 let 声明的作用域仅限于该块。
<script> 
 let name = 'Nicholas'; 
 let age = 36; 
</script> 
<script> 
 // 假设脚本不确定页面中是否已经声明了同名变量
 // 那它可以假设还没有声明过
 if (typeof name === 'undefined') { 
 let name; 
 } 
 // name 被限制在 if {} 块的作用域内
 // 因此这个赋值形同全局赋值
 name = 'Matt'; 
 try { 
 console.log(age); // 如果 age 没有声明过,则会报错
 } 
 catch(error) { 
 let age;
  } 
 // age 被限制在 catch {}块的作用域内
 // 因此这个赋值形同全局赋值
 age = 26; 
</script>
var name = 'Matt'; 
console.log(window.name); // 'Matt' 
let age = 26; 
console.log(window.age); // undefined

const

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。

const foo = {};

// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123

// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

ES6 声明变量的六种方法

ES5 只有两种声明变量的方法:var命令和function命令。ES6 除了添加let和const命令,后面章节还会提到,另外两种声明变量的方法:import命令和class命令。所以,ES6 一共有 6 种声明变量的方法

# 数据类型

计算机顾名思义就是可以做数学计算的机器,因此,计算机程序理所当然地可以处理各种数值。但是,计算机能处理的远不止数值,还可以处理文本、图形、音频、视频、网页等各种各样的数据,不同的数据,需要定义不同的数据类型。

在 JavaScript 中定义了以下几种数据类型:

  • ECMAScript 有 6 种初始类型(primitive type),即 UndefinedNullBooleanNumberString和 ES6 新增的Symbol 以及 ES2020新增的数据类型
  • ECMAScript 有 1 种引用类型,即Object
分类 类型
初始类型 undefined undefined
null 一个表明 null 值的特殊关键字。 JavaScript 是大小写敏感的,因此 null 与 Null、NULL或其他变量完全不同。
string 单双引号引起来的数据
number 包括整型和浮点型。科学计数法。支持二进制、八进制、十进制、十六进制。
BigInt(大整数) ES2020 引入了一种新的数据类型 BigInt(大整数),这是 ECMAScript 的第八种数据类型。BigInt 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。
boolean true false
Symbol 表示独一个一无二的值。(ES6新增)
引用类型 Object 包含相关属性和方法的一个集合。包含Function、Array、Object

# undefined

undefined 未定义,指的就是变量创建后但是没有赋值,而变量的默认值就是undefined.

如:

var a;
console.log(a); // undefined

# null

null 一个表明 null 值的特殊关键字,相当于一个占位符。JavaScript 是大小写敏感的,因此 null 与 Null、NULL 或其他变量完全不同。

Null 类型同样只有一个值,即特殊值 null。逻辑上讲,null 值表示一个空对象指针,这也是给 typeof 传一个 null 会返回"object"的原因:

let car = null; 
console.log(typeof car); // "object"

在定义将来要保存对象值的变量时,建议使用 null 来初始化,不要使用其他值。这样,只要检查 这个变量的值是不是 null 就可以知道这个变量是否在后来被重新赋予了一个对象的引用,比如:

if (car != null) { 
 // car 是一个对象的引用
} 

undefined 值是由 null 值派生而来的,因此 ECMA-262 将它们定义为表面上相等,如下面的例 子所示:

console.log(null == undefined); // true 

用等于操作符(==)比较 null 和 undefined 始终返回 true。但要注意,这个操作符会为了比较 而转换它的操作数 即使 null 和 undefined 有关系,它们的用途也是完全不一样的。如前所述,永远不必显式地将 变量值设置为 undefined。但 null 不是这样的。任何时候,只要变量要保存对象,而当时又没有那个 对象可保存,就要用 null 来填充该变量。这样就可以保持 null 是空对象指针的语义,并进一步将其 与 undefined 区分开来。 null 是一个假值。因此,如果需要,可以用更简洁的方式检测它。不过要记住,也有很多其他可 能的值同样是假值。所以一定要明确自己想检测的就是 null 这个字面值,而不仅仅是假值。

let message = null; 
let age; 
if (message) { 
 // 这个块不会执行
} 
if (!message) { 
 // 这个块会执行
} 

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

# string 类型

用单双引号来说明,他所包围的值都可以是字符串。

引号包裹的字符串中间不允许换行

单双引号的用法:

  • 效率是一样的
  • 只能成对出现,不能相互交叉使用
  • 可以相互嵌套。

还包括一些特殊的字符:

字符 描述
\0 Null 字节
\b 退格符
\f 换页符
\n 换行符
\r 回车符
\t Tab (制表符)
\v 垂直制表符
' 单引号
" 双引号
\ 反斜杠字符(\)
\XXX 由从 0 到 377 最多三位八进制数 XXX 表示的 Latin-1 字符。例如,\251 是版权符号的八进制序列。
\xXX 由从 00 和 FF 的两位十六进制数字 XX 表示的 Latin-1 字符。例如,\ xA9 是版权符号的十六进制序列。
\uXXXX 由四位十六进制数字 XXXX 表示的 Unicode 字符。例如,\ u00A9 是版权符号的 Unicode 序列。见 Unicode escape sequences (Unicode 转义字符).
\u{XXXXX} Unicode 代码点 (code point) 转义字符。例如,\u{2F804} 相当于 Unicode 转义字符 \uD87E\uDC04 的简写。

# number 类型

number 类型包括整型和浮点型。支持十进制(基数为 10)、十六进制(基数为 16)、八进制(基数为 8)以及二进制(基数为 2)表示。

进制 表示
十进制 十进制整数字面量由一串数字序列组成,且没有前缀 0。
八进制 八进制的整数以 0(或 0O、0o)开头,只能包括数字 0-7。
十六进制 十六进制整数以 0x(或 0X)开头,可以包含数字(0-9)和字母 a~f 或 A~F。
二进制 二进制整数以 0b(或 0B)开头,只能包含数字 0 和 1。
var n1 = 100; //十进制

var n2 = 071;  //八进制
var n3 = 0o71; //八进制 ES6规定 0o开头

var n4 = 0xa9; //十六进制

var n5 = 0b11; //二进制

严格模式下,八进制整数字面量必须以 0o 或 0O 开头,而不能以 0 开头。

对于非常大或非常小的数,可以用科学计数法表示,可以把一个数表示为数字(包括十进制数字)加 e(或 E),后面加乘以 10 的倍数。例如:

# 用科学计数法来表示,还包括一些特殊的值:
# 最大值
Number.MAX_VALUE    //1.7976931348623157e+308

# 最小值
Number.MIN_VALUE    //5e-324

var n1 = 3e2; //3*100 = 300
var n2 = 5e3; //5*1000 = 5000
var n3 = 5.3e3; //5.3*1000 = 5300

# 无穷大
Infinity    // 1/0 = Infinity

# BigInt 类型 ES2020

TIP

JavaScript 所有数字都保存成 64 位浮点数,这给数值的表示带来了两大限制。一是数值的精度只能到 53 个二进制位(相当于 16 个十进制位),大于这个范围的整数,JavaScript 是无法精确表示的,这使得 JavaScript 不适合进行科学和金融方面的精确计算。二是大于或等于2的1024次方的数值,JavaScript 无法表示,会返回Infinity。

// 超过 53 个二进制位的数值,无法保持精度
Math.pow(2, 53) === Math.pow(2, 53) + 1 // true

// 超过 2 的 1024 次方的数值,无法表示
Math.pow(2, 1024) // Infinity

ES2020 引入了一种新的数据类型 BigInt(大整数),来解决这个问题,这是 ECMAScript 的第八种数据类型。BigInt 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。

const a = 2172141653n;
const b = 15346349309n;

// BigInt 可以保持精度
a * b // 33334444555566667777n

// 普通整数无法保持精度
Number(a) * Number(b) // 33334444555566670000

TIP

为了与 Number 类型区别,BigInt 类型的数据必须添加后缀n。

1234 // 普通整数
1234n // BigInt

// BigInt 的运算
1n + 2n // 3n

BigInt 同样可以使用各种进制表示,都要加上后缀n

0b1101n // 二进制
0o777n // 八进制
0xFFn // 十六进制

BigInt 与普通整数是两种值,它们之间并不相等。

42n === 42 // false

typeof运算符对于 BigInt 类型的数据返回bigint

typeof 123n // 'bigint'

BigInt 可以使用负号(-),但是不能使用正号(+),因为会与 asm.js 冲突。

-42n // 正确
+42n // 报错

JavaScript 以前不能计算70的阶乘(即70!),因为超出了可以表示的精度。

let p = 1;
for (let i = 1; i <= 70; i++) {
  p *= i;
}
console.log(p); // 1.197857166996989e+100

现在支持大整数了,就可以算了,浏览器的开发者工具运行下面代码,就OK。

let p = 1n;
for (let i = 1n; i <= 70n; i++) {
  p *= i;
}
console.log(p); // 11978571...00000000n

# boolean 类型

Boolean. 布尔值,true 和 false. 表示"真"和"假"

# Symbol es6

Symbol ( 在 ECMAScript 6 中新添加的类型).。一种数据类型,它的实例是唯一且不可改变的。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。

# Object

引用类型: 引用类型是一种数据结构,用于将数据和功能组织在一起,存储大量的数据

三大引用类型: Object 、Array 、Function

# 判断数据类型

当我们想知道一个变量中存储的数据类型是什么,我们可以通过 typeof 运算符。

# 语法

typeof运算符后跟操作数:

typeof operand;
// or
typeof operand;

# 返回值

下表总结了typeof可能的返回值

类型 结果
Undefined "undefined"
Null "object"
Boolean "boolean"
Number "number"
String "string"
Symbol "symbol"
BigInt "bigint"
函数对象 "function"
任何其他对象 "object"

WARNING

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

# 示例

// typeof总是返回一个字符串
typeof typeof 1 === "string";

// Number
typeof 37 === "number";
typeof NaN === "number";
typeof Math.PI === "number";

// String
typeof "" === "string";
typeof "bla" === "string";

// Object
typeof { a: 1 } === "object";
typeof [1, 2, 4] === "object";
typeof null === "object";

# 变量在内存划分

变量类型分两种是由于其在内存中存储方式不同:基本类型的数据是存放在内存中的,而引用类型的数据是存放在内存中的

基本类型与引用类型的存储方式

# 堆栈的概念

两者都是存放临时数据的地方。

#

栈(数据结构):一种先进后出的数据结构。

栈区(stack) 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。

栈

这种乒乓球的存放方式与栈中存取数据的方式如出一辙。处于盒子中最顶层的乒乓球 5,它一定是最后被放进去,但可以最先被使用。而我们想要使用底层的乒乓球 1,就必须将上面的 4 个乒乓球取出来,让乒乓球 1 处于盒子顶层。这就是栈空间先进后出,后进先出的特点。图中已经详细的表明了栈空间的存储原理。

#

堆(数据结构):堆可以被看成是一棵树,如:堆排序;

堆区(heap) 一般由程序员分配释放,若程序员不释放,程序结束时可能由 OS 回收。

堆数据结构是一种树状结构。它的存取数据的方式,则与书架与书非常相似。

书虽然也整齐的存放在书架上,但是我们只要知道书的名字,我们就可以很方便的取出我们想要的书,而不用像从乒乓球盒子里取乒乓一样,非得将上面的所有乒乓球拿出来才能取到中间的某一个乒乓球。好比在 JSON 格式的数据中,我们存储的 key-value 是可以无序的,因为顺序的不同并不影响我们的使用,我们只需要关心书的名字

堆是在程序运行时,而不是在程序编译时,申请某个大小的内存空间。即动态分配内存,对其访问和对一般内存的访问没有区别。对于堆,我们可以随心所欲的进行增加变量和删除变量,不用遵循次序。

# 两种数据类型的存放

# 栈区存储

JavaScript 的基础数据类型往往都会保存在变量对象中,即保存在栈内存中,因为这些类型在内存中分别占有固定大小的空间,通过按值来访问。 基本类型由于数据简单,会存放到内存栈区,栈区包括了 变量的标识符和变量的值:
栈区示意图

栈区: 长度固定,读取速度快

# 堆区存储

JS 的引用数据类型,比如数组 Array,Object,它们值的大小是不固定的。引用数据类型的值是保存在堆内存中的对象。JavaScript 不允许直接访问堆内存中的位置,因此我们不能直接操作对象的堆内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。因此,引用类型的值都是按引用访问的。这里的引用,我们可以理解为保存在变量对象中的一个地址,该地址与堆内存的实际值相关联。

我们可以结合以下例子与图解进行理解:

堆区示意图

堆区示意图

堆区: 长度不固定,读取速度慢

# 变量复制

# 基本数据类型

基本数据类型:基本数据类型值指保存在栈内存中的简单数据段。访问方式是按值访问。

var a = 1;

栈区示意图1

操作的是变量实际保存的值。

a = 2;

栈区示意图2

基本类型变量的复制:从一个变量向一个变量复制时,会在栈中创建一个新值,然后把值复制到为新变量分配的位置上。

var b = a;

栈区示意图3

b = 2;

栈区示意图4

# 引用数据类型

引用数据类型:引用数据类型值指保存在堆内存中的对象。也就是,变量中保存的实际上的只是一个指针,这个指针指向内存中的另一个位置,该位置保存着对象。访问方式是按引用访问。

var a = new Object();

堆区示意图1

当操作时,需要先从栈中读取内存地址,然后再延指针找到保存在堆内存中的值再操作。

a.name = 'xz';

堆区示意图2

引用类型变量的复制:复制的是存储在栈中的指针,将指针复制到栈中未新变量分配的空间中,而这个指针副本和原指针指向存储在堆中的同一个对象;复制操作结束后,两个变量实际上将引用同一个对象。因此,在使用时,改变其中的一个变量的值,将影响另一个变量。

var b = a;

堆区示意图3

b.sex = 'boy';

堆区示意图3

console.log(a.sex) //boy
console.log(b.sex) //boy