JS语言基础

14 阅读15分钟

1. 变量和声明

1.1 引言

编程语言的本质,就是通过代码来指示计算机应该如何操作数据,并进行运算从而得到我们想要的结果。而数据可以存在内存中、也可以存在硬盘。绝大部分的编程语言(除了数据库查询语言)操作的数据几乎都是在计算机内存中,因为从内存中读写数据是非常高效的,而且程序一般都是临时性的,这些数据通常不需要长期存储,有程序运行,就有程序关闭,程序关闭后,内存中的数据就会得到及时释放。而数据库通常是持久化存储,需要完整备份数据信息,因而一般都存在硬盘上。

那么内存中的数据如何定义呢?

任何语言都需要通过变量来定义内存中的数据, 通过变量才可以标识数据,从而在程序中引用、修改、传递这些数据。需要注意的是,不同的编程语言声明变量管理变量赋值变量的方式是不同的,但是无论如何,其基本思想以及底层逻辑都是一样的。

1.2 变量概述

变量可以类比人名,只是一个代号,代表的是一个具体的人。在程序中,变量是标识某个数据(值)的符号。专业点来说,变量名叫标识符

1.3 变量声明的方式

JS中,有三种方式可以声明变量。分别是varletconst共三个关键字

  • varJS早期声明变量的方式,现代化的开发实践中已经不推荐使用了,但了解它依然是很有必要的
  • letES6推出的声明变量的方式,用于声明一个具有块级作用域的变量
  • const也是ES6推出的,声明一个具有块级作用域的只读常量

变量的声明在底层会涉及到预编译部分,为了避免混淆,在基础部分的章节的讲解中(从数据结构开始),统一使用var来声明变量,我们将在学习完基础后,才使用除var以外的声明方式,并着重说明其区别。

1.4 变量声明的过程
var a = 10;
let b = 10;
const c = 10;

上面的案例中,我们使用了三种方式声明变量,我们以var a = 10;举例:

声明变量的过程拆解

声明和赋值本质上是两部分,或者说两条语句,先有声明再有赋值。因此var a = 10,实际上是两部分

  • var a 表示声明一个变量a,因此这一部分称为声明
  • = 10 表示把10赋值给变量a=是赋值运算符,因此这一部分称为初始化器赋值

我们可以根据实际情况选择只对变量声明或者声明且赋值。JS变量声明后,允许动态修改其数据类型,因此变量声明后不赋值,这个变量的值将引擎设置为undefined,表示未定义。

需要注意的是,如果使用const方式,声明的将是一个常量,而不是变量,常量声明后,程序运行过程中不能动态修改,并且声明时就必须要赋值,否则将引发引擎词法分析错误,导致异常。

var a;
console.log(a) // undefined
let a;
console.log(a) // undefined
const a; // Uncaught SyntaxError: Missing initializer in const declaration
console.log(a) // 并不会执行该段代码

上面三段代码,分别使用三种方式来声明变量,但是声明后,均没有赋值。其中varlet 关键字声明的变量,允许声明后不赋值,因为它们是可变的。因此,在使用console.log()输出时,代码是正常运行的,只不过输出的变量值是undefined。但是使用const声明变量不赋值,console.log(a)是不会执行的,因为引擎在词法分析阶段检测到声明了常量,但是未赋值,这不符合JS语法标准规范,所以引擎直接抛出了语法错误,从而导致线程中断,无法继续解析脚本,进行执行。

Uncaught SyntaxError: Missing initializer in const declaration

  • 该错误表示在const声明中缺少初始化式 - 意味着使用const声明变量必须要赋值。
1.5.1 语句

源代码会被转为机器指令去执行,因此指令实际上就是我们抒写的代码语句,引擎要把语句进行解析编译后才能形成CPU可执行的机器指令。

上面示例中var a;是一条语句,var a = 10;也是一条语句。

语句要用分号;结尾。一条语句独占一行,如果多条语句放一行的话,必须用;隔开。

ECMAScript规定语句末尾必须要自动插入;,大多数引擎都已经实现自动补全分号。因此实际抒写代码时,我们可以不加;,但是为了保证最佳实践,我们还是手动加上;

1.5.2 标识符

标识符,标识的就是数据,除了上面说的变量名以外,后续要讲到的函数、函数参数、对象属性等,都是标识符。

保留字和关键字不能作为标识符。

1.5.3 字符集

JS使用Unicode字符集,因此标识符通常可以以字母下划线_美元符号$开头。后面可以跟上数字。同时ECMA规范中明确指出一切内容都要严格区分大小写。

Unicode是一个字符编码标准,涵盖了世界上现存所有字符:符号、数字、表情等,旨在为世界上所有语言字符提供统一的编码方案。ASCII汉字集等都是Unicode的子集。

1.5.4 注释

JS中的注释和C++以及其他主流语言都很类似,注释的作用是为我们标记代码,从而提供可读说明和注解,便于维护。注释的行为类似于空白字符,引擎解析时会忽略它,不影响代码执行。

JS中注释分为单行和多行两种:

  • 单行 采用// 双斜杠
// console.log(a)
  • 多行 采用/* */
/**
 * 这是多行注释
 * 这是多行注释
 */
1.5.5 字面量

字面量其实就是,直接在代码中表示某种类型的值,比如:数字、布尔、数组、对象等。由于不是通过变量或者表达式计算而出的。因此也叫字面量。后面我们将会学习一些内置函数来创建我们想要的对象,这里要说明的是,字面量形式的写法,他们在底层并不会隐式的调用内置的构造函数来创建想要的数据类型。因为引擎在解析时,会自动根据语法来构造这些我们想要的数据。因此字面量是非常高效简洁的。不过有的时候,我们可能也需要内置函数来创建。

var a = 10; // 数字字面量
var b = 'hello' // 字符串字面量
// 对象字面量
var c = {
  name:'小红'
}

2. 数据类型概述

2.1 引言

数据类型是每一门编程语言的核心,用于定义变量的性质,不同类型的数据在计算机内存中有不同的存储方式,并且数据类型决定了变量可以执行哪些操作。使用合适的数据类型可以帮助程序高效的分配内存资源。也使得开发者更容易理解代码逻辑。

最新的ECMAScript定义了8种数据类型,这8种数据类型可以分为两类:

原始类型 - 七种

  • Boolean: 布尔值,通过truefalse表示
  • Number: 数字值,JS中数字不区分整数和浮点数
  • String: 字符串值,用双引号""或单引号''包裹
  • undefined: 表示未定义
  • null:空值,无特殊含义
  • BigInt:任意精度的大整数,是ES6之后新增的类型
  • Symbol: 符号,其实例是唯一不可变的类型 是ES6新增的类型

对象类型 - 一种

  • Object: 范围比较宽泛,对象、数组、函数等结构化的数据都是对象,函数非常特殊
2.2 数据类型的简单示例
var a = 10; // number类型

var a_float = 1.1 // number类型

var str = 'string' // string类型

var un = undefined; // undefined类型

var nu = null; // null类型

// Object类型
var obj = {
  name:'小刚',
  age:16
}
var arr = [1,2,3] // 数组 Object类型

// 函数 Object类型
function test () {
  
}
2.3 动态类型和静态类型

每门编程语言,都有着自己的特性。对于JS而言,除了闭包、原型、函数式等核心特性外,动态数据类型就是它最大的特性。动态数据类型指的是,在程序运行过程中,可以随意修改变量的数据类型

var a = 10;
a = 'Hello'

首先把a声明赋值为10,这是一个数字类型的数据,随后再把a重新赋值为'Hello',这是一个字符串类型的数据。这在JS中是完全合法的,也正因为如此,JS被称为动态数据类型的语言,具有极高的灵活性,但是这种灵活性往往在大型应用程序中更容易造成潜在的错误,导致难以排查,这也是TypeScript目前越来越流行在前端领域的原因。而静态类型的语言,比如Java,变量声明后必须指定数据类型,并且程序运行过程中,不允许修改数据类型,这也是Java代码健壮性高的原因之一。总的来说动态类型和静态类型语言各有千秋,只要掌握好都是非常强大的。

3. 运算符概述

3.1 引言

程序中,数据的运算是通过指定运算符(也称操作符)来实现的。运算符也是语言的核心概念之一,不同类型的运算符,表示计算机会采用不同的规则去进行数据计算。需要注意的是,前面提到过,数据类型决定了变量可以执行哪些操作,也就是说运算符并不是所有数据类型公用的,如果数据类型不支持某种运算符,将会引发程序错误。

下面我们列出实际开发中,最常用的运算符:

3.2 算数运算符

适用于对数值数据进行数学运算,比如整数、浮点数之类的运算,不过JS中不区分整数和浮点数

运算符适用数据类型描述示例结果
+数字、字符串拼接加法5 + 38
-数字减法5 - 32
*数字乘法5 * 315
/数字除法5 / 22.5
%数字求模(取余)5 % 21
**数字幂运算2 ** 38
var a = 5 + 3; // a = 8

var b = 5 - 3; // b = 2

var c = 5 * 3; // c = 15

var d = 5 / 2; // d = 2.5

var e = 5 % 3; // e = 1

var f = 2 ** 3; // f = 8

在开发中,通常数据都是动态的,来自于后端服务器,或者其他第三方平台等。一般不会通过定死的数据值进行运算,大多数情况下都是通过变量来表示数据。

var a = 10;
var b = 10;

// 运算求值
var c = a + b;
var d = a - b;
var e = a * b;
// ...
3.2.1 递增递减运算符

ECMA的递增递减操作符直接照搬C语言,但是分了两种情况:前缀版(位于变量前)和后缀版(位于变量后)。

  • ++ 表示递增
  • --表示递减
位置行为返回值
前置递增/递减变量先增/减,再参与表达式运算递增或者递减后的新值
后置递增/递减变量先参与当前表达式的计算,再进行计算递增或递减前的原始值
var n1 = 2;
console.log(++n1) // 3

var n2 = 2;
console.log(n2++) // 2

var n3 = 2;
var d = n3-- + ++n3 + n3++ + --n3;
// n3--(先返回再运算) 2,运算后为1
// ++n3(先计算再返回) 2,因此运算也为2
// n3++ (先返回,在运算) 2,运算后得3
// --n3(先计算,再返回) 2
// d 最终得到 8 

💁 温馨提示

这种针对一个数值进行操作的操作符也叫一元操作符

除了自增/自减,上面列出的+-操作符,如果放在某个变量前面,也称为一元加/一元减,在底层,这种操作会自动执行Number()转换操作,+ 、-就表示正负,至于转换规则,就是下面Number类型中的转换规则。基于这个特性,一元加减操作符除了用于基础的算数运算,也可用于数值类型的转换。

3.3 赋值运算符

赋值运算符,顾名思义用于给变量进行赋值操作,不过只要带了数学运算的赋值运算符,一般还是只能用于数字类型,=则可适用于几乎所有的类型。

运算符适用数据类型描述示例结果
=几乎所有直接赋值x = 8x的值为8
+=数字、字符串加后赋值x += 2x表示的值加上2后再赋给x
-+数字减后赋值x -= 3x表示的值减3后再赋给x
*=数字乘后赋值x *= 2x表示的值乘以2后再赋给x
/=数字除后赋值x /= 2x表示的值除以2后再赋给x
%=数字求模赋值x %= 2x表示的值对2求模后再赋给x
3.4 比较运算符

用于比较两个值,得到的结果是一个布尔值(true | false),一般适用于数字、字符串、布尔值,这类运算符大多用于条件判断,或者某个逻辑表达式。

运算符描述示例结果
==等于5 == 3false
!=不等于5 != 2true
>大于5 > 3true
<小于5 < 3false
>=大于等于5 >= 3true
<=小于等于5 <= 2false
3.5 逻辑运算符

用于对布尔值进行逻辑操作。会触发JS隐式数据类型转换

运算符描述示例结果
&&逻辑与 Andtrue && falsefalse
``逻辑或 or`52`5
逻辑非!truefalse
!!逻辑双非!! falsefalse
3.6 表达式

算符运算符通常情况下都是根据两个值进行运算,得到某一个值,例如10 + 10得到20。因此,10 + 10也称表达式表达式的本质就是通过运算得到某个值。这个值我们可以通过变量去接收,以便后续使用,或者直接用于条件判断函数参数或者什么也不做。

算数运算中,表达式的值一般称为数学值

比较运算中 表达式的值一般称为逻辑值/布尔值

语句是没有结果的,这是它和表达是最大的区别

  • 表达式是代码中的一个核心概念,通过运算得到一个结果。
  • 运算符是表达式的关键组成部分,用于对数据进行操作。
  • 表达式的结果可以由变量接收,也可以直接用于条件判断或传递给函数、在或者什么也不做。
  • 数据类型决定了变量可以使用哪些运算符,也就是表达式的合法性。

javascrip中,算数运算遵循现代数学的运算规则和优先级。因此对于一些复杂的表达式,在语法层面,我们可以使用括号()包裹以提升优先级,比如 1 + 1 * 20,我们想优先计算1 + 1再乘20时,可以写作(1 + 1) * 20

4. 数据类型详解

返回值含义
"undefined"表示该值的类型是undefined未定义
"boolean"示该值的类型是布尔值
"string"表示该值的是字符串
"number"表示该值的类型是数字
"object"表示该值的类型是对象或者null
"function"表示该值的类型是函数
"symbol"表示该值的类型是符号
var a; // 变量未赋值 - undefined类型
var b = true; // 布尔类型
var c = 10; // 数字类型
var d = '10' // 字符串类型
var e = null; // null类型
function test () {} // 对象类型

console.log(typeof a) // "undefined"
console.log(typeof b) // "boolean"
console.log(typeof c) // "number"
console.log(typeof d) // "string"
console.log(typeof e) // "object"
console.log(typeof test) // "function"
// 加括号调用
console.log(typeof (1 + 1)) // "number"

💁 温馨提示

typeof是一个操作符,并不是函数,因此使用时不用加括号传参调用,但是上例中,最后一段代码使用了括号依然不报错,这是因为typeof操作的是一个值,而(1 + 1)是一个表达式,表达式正是要得到一个结果值,因此加上括号也是可以的。

有两个点需要注意:

  • 函数属于对象类型的数据,但是由于它在JS中的特殊性,因此typeof将其返回为"function",为了便于我们标识。
  • null是原始数据类型的中一种,但是typeof返回"object"
    • 这是一个历史遗留问题,在早期的ECMA规范中,null并不属于原始类型,它表示的是一种特殊的对象,即空对象的引用(指向一个不存在的引用地址),因此它被标识为"objec"。但是从语义上来说,null是无意义的值,因此在ES3后,把它明确为原始类型的数据,但是为了兼容性,使用typeof操作null值依然返回"object"
    • null只是设计上存在一定缺陷,但是实际上是非常有用的,在表示无值或者空引用时,推荐使用,需要注意的是它和undefined的区别,有时候容易混淆。
    • null表示一个空对象引用,即这个对象不存在,因此通过null访问某个不存在的属性时会报错

4.2 Undefined类型

Undefined类型只有一个值,就是undefined。当我们使用varlet关键字声明变量但是没有进行赋值时,那么变量的值就是undefined,这是引擎帮我们自动赋的。如下:

var message;
console.log(typeof message) // "undefined"
console.log(message == undefined) // true

变量声明未赋值和变量未声明(也就是未定义)是有区别的。如下:

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

console.log(age) // 报错,因为age没有声明,引擎无法找到该标识

对于未声明的变量,只能执行一种操作,那就是typeof,它依然返回"undefined"。这里有点模糊,主要是因为在逻辑来说变量声明未定义变量未声明没有太大区别,因此typeof没有报错。这里只是作为一个补充知晓即可,实际开发中应当避免这种情况,遵循最佳实践,变量使用前要声明。

实际上未声明的变量执行delete操作也不会报错,这涉及到了对象属性的操作,后面再讲,且注意严格模式。

typeof a // "undefined"

💁 温馨提示

无论变量声明了未赋值,还是未声明变量,执行typeof都会输出"undefined",但是如果使用了未声明的变量,例如console.log是会引发报错的,要切记。

这里建议,声明变量后如果不及时的赋予某个值,可以显示的赋为undefined

4.3 Null类型

Null类型同样只有一个值,就是特殊值nullnull的争议一直以来都是数据类型的问题。但其作用依然是遵循早期的设计目的,表示一个空对象的引用。当我们要定义变量保存对象值时,初始化时建议赋值为null,表示一个特殊的占位,以便明确其寓语义。当然,如果对象一开始就确定数据,我们直接通过字面量抒写即可。

var obj = null; // 定义一个obj为null,将来填入真实的对象数据
obj.name = '哈哈'

undefined实际上也是由null派生而来的,因此ECMA规范将他们定义为表面上相等

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

但是他们的用途是完全不一样的,使用时,应当遵循其语义,确保最佳实践

undefinednull在布尔值转换时,均是假值false

4.4 Boolean类型
4.4.1 引言

布尔值(Boolean),在几乎所有的编程语言中都是使用最频繁的数据类型之一,直接使用两个字面值表示即可。分别是:true | false

布尔值是基于布尔代数发展而来,布尔代数的创始人是英国数学家乔治 · 布尔。因此为了纪念他,将布尔值命名为布尔。在ECMA规范中,布尔值用的是小写的truefalse。这一点要注意,因为在某些语言中,比如Python中布尔值是TrueFalse,首字母是大写的。

因此呢,JS中可以使用TrueFalse是无效布尔值,但可作为标识符,但是为了语义化,不要这么做。

💁 温馨提示

虽然布尔值只有两个,但是所有ECMAScript数据类型的值都有一个等价的布尔值,也就是说可以通过转换的方式,把任意非布尔类型的数据转为布尔值,这也是布尔值具有难度的地方。

转换方式:内置函数 Boolean()和逻辑运算符

var str = 'hello'; // 定义一个字符串值是hello
var booStr = Boolean(str) // 调用布尔函数把str传入转为布尔值 存在booStr中
4.4.2 转换规则

下面我们列出一个表格来看看不同数据类型转换为布尔值后具体得到什么值。

数据类型得到true的值得到false的值
Booleantruefalse
String非空字符串空字符串 - ""
Number非0值(包括负数和无穷值)0、NaN
Object任意对象null
Undefinedundefined

JS中,这些转换规则是非常重要的,以及后面讲到的隐式类型转换。因为在条件判断等流程控制语句中会自动执行数据类型的布尔值转换。只有理解这些转换规则,才能确保在条件判断或逻辑运算时不出错。布尔的实质就是表示逻辑值的真值假值,直白点来说就是

var a1 = true;
var a2 = false;
var str1 = 'hello';
var str2 = '';
var n1 = 0;
var n2 = 1;
var n3 = -1;
var obj = {};
var un;
function test () {};

console.log(Boolean(a1)) // true
console.log(Boolean(a2)) // false
console.log(Boolean(str1)) // true
console.log(Boolean(str2)) // false
console.log(Boolean(n1)) // false
console.log(Boolean(n2)) // true
console.log(Boolean(n3)) // true
console.log(Boolean(obj)) // true
console.log(Boolean(un)) // false
console.log(Boolean(test)) // true

-1是真值,要注意!

4.4.3 逻辑运算符的转换

逻辑非!: 始终返回布尔值,首先将操作数转为布尔值,然后取反

var a = '';
!a // true

var b = '123'
!b // false

逻辑双非 !!: 相当于直接调用Boolean()函数,把值转为等价的布尔值


⚠️

逻辑与 &&: 相对复杂一点,用于两个值的处理,逻辑与操作符可用于任何类型的操作数,不限于布尔值。如果有操作数不是布尔值,则逻辑与不一定会返回布尔值。

逻辑与操作符遵循短路规则:

  • 从左到右依次计算操作数
  • 如果某个操作数为false(或等价false的值),&&会立刻返回值,后续操作数不再计算
  • 如果所有操作数都为true&&返回最后一个操作数的值

逻辑与返回值:

  • 第一个操作数为假值,直接返回第一个操作数。
  • 第一个操作数为真值,继续计算下一下操作数,遇到假值就返回,或者返回最后一个真值

总结一句话就是:遇假值返回假值,所有都为真值,返回最后一个真值

如果还不好理解的话,站在现实的角度上来说,逻辑与表示的就是并且的意思,指的是某种情况并且某种情况,表示的是一种肯定,多种条件满足的的情况。如果第一个条件都不满足的话,那就不存在并且的说法。只不过在程序的角度上,逻辑表达式一般需要返回结果。

var a = 0 && 1 && 2; // 0,因为0是假值,直接返回,不再计算后面的值
var b = 1 && 2 && 3 && -1 && 0; // 0,前面四个均是真值,遇到0是假值,所以返回
var c = [] && {}; // {},数组和对象均为真,返回数组

逻辑或||: 和&&类似,也不一定返会布尔值,要看操作数,同样遵循短路规则,两者一正一反。

逻辑或返回值:

  • 第一个操作数为真值,直接返回第一个真值
  • 第一个操作数为假值,则继续计算下一个操作数,遇到真值就返回,或者返回最后一个假值

总结一句话就是:遇真值返回真值,所有都为假值,返回最后一个假值

同理,站在现实的角度上来说,逻辑或就是或者,或者表示的是多种情况满足一种即可,因此如果说有多种情况,每种情况都会查看,直到某种情况满足。

var a = 1 || 2 || 3; // 1
var b = 0 || 2; // 2
var c = {} || [] // {}
var d = null || undefined || [] // []

逻辑运算符的的场景,大多是用于条件判断。用于条件判断时,无需关注其返回值。因为条件判断语句会自动执行布尔操作。还有一种场景是用于变量存储,就是通过逻辑判断得到某个值赋给某个变量以便后续使用。

对于比较操作符也是如此,不过比较操作比较简单,只针对数值。

4.4.4 相等操作符的转换

判断两个变量是否相等,在开发中是非常常见的。在ECMA规范中提供了两组操作符。

  • 第一组是等于 == 和不等于 !=,操作数相等则会返回true,不等则返回true,反之则是false
  • 第二组是全等===和不全等 !==,操作相等则会返回true,不等则返回true,反之则是false

唯一的不同在于-等于和不等于会发生类型转换,再进行比较。而全等和不全等不会发生类型转换。这就是JS中大名鼎鼎的强制类型转换、也叫隐式类型转换。

下面将列出,隐式类型转换的规则:。

  • 如果任意一个操作数是布尔值,则将其转换为数值再比较是否相等。
  • 如果一个操作数是字符串,另一个操作数是数值,则尝试将字符串转为数值,再比较是否相等。
  • 如果一个操作数是对象,另一个不是,则调用对象的valueOf(),获取其原始值,再根据前面的规则进行比较
  • nullundefined相等。
  • nullundefined不能转换为其他类型的值再进行比较。
  • 如果有任一操作数是NaN,则相等操作符返回false,不相等操作符返回true
  • 即使两个操作数都是NaN,相等操作符也返回false,因为按照规则,NaN不等于NaN
  • 如果两个操作数都是对象,则比较它们是不同一个对象。如果都指向同一个对象,则相等操作符返回true,否则两者不相等。

null值在隐式情况下不会发生转换,但是在其他数值比较运算中,会被转为0


下面列举几个简单的例子:

表达式结果
null == undefinedtrue
'NaN' == NaNfalse
null == 0false
undefined == 0false

这些规则需要熟记,数据类型的转换规则,应该是JS中唯一需要死记硬背的,随着实践经验就会越发熟练。

实际开发中,推荐使用全等和不全等,因为它们会严格检验数据类型,如果数据类型不一致则返回false。这样可以确保代码健壮性。非要使用等于或不等于的话,需要确保其转换规则的深入理解。

全等情况下,null不等于undefined

4.5 Number类型
4.5.1 引言

在某些语言中,会把浮点数和整数作为两种不同类型的数据。但是在JS中,数值类型的数据称为Number类型。不区分整数和浮点数,采用了IEEE754表示整数和浮点数,这种表示法可以称为双精度表示法。

💁 温馨提示

  • 实际情况下,可能会存在+0-0,在JS中这两者都表示0
  • 抒写数字值最常见的方式就是直接采用十进制字面量的写法即可,也可以采用八进制和十六进制,不过这里不涉及,实际开发中也很少会用到,后面在讲进制时才讲解。
4.5.2 浮点数

所谓浮点,指的是数字中必须包含小数点,并且小数点的前后都一定要有有效数字。

var a = 1.1;
var b = 0.1;
var c = .1; // 这种方式,小数点前没有数字,引擎会自动加上,但是这种方式不推荐

由于浮点数使用的内存空间是整数的两倍,所以ECMA规范把浮点数均转为整数值,这里涉及到几个细节:

  • 小数点后没有数字,数值会被转位整数,例如1.实际上处理后就是1
  • 小数点后有数字,但是是0,例如1.0,实际上1.0就是整数,因此也会被处理成1

对于非常大或者非常小的浮点数/整数,都可以采用科学计数法表示,就是某个基数乘以10多少次幂

var a = 3.1e3; // 3100

还需要补充一点的是,针对浮点数的运算存在精度问题,任何语言都存在。因为计算机采用二进制表示所有数据,对于某些浮点数不能通过二进制精确表示,而计算机的近似处理方式就会导致浮点数运算进度丢失。这一部分可以阅读Python文档,有详细说明。一般使用第三方库处理或者语言内置模块,不过JS没有内置处理精度的模块,一般采用第三方库。

4.5.3 值的范围

受限于内存,JS并不能支持世界上所有的数字值,因此这里有一些详细的规则说明。

  • 通过Number.MIN_VALUE获取支持的最小值
  • 通过Number.MAX_VALUE获取支持的最大值
  • 如果某个计算得到的数值结果超出了上面最大或最小的边界,那么得到的将会是无穷值Infinity
  • Infinity有两种结果,分别是正负无穷 +Infinity-Infinity
  • 如果值是Infinity,那么将无法进行二次运算,因为没有表示的方式了。
  • 要判定一个数值是不是介于JS所支持的最小值和最大值之间,通过内置的isFinite()方法

所有编程语言提供的数值边界范围几乎可以满足现代化开发中的绝大部分需求,因此这一部分了解即可,ES6之后还提供了BigInt大整数类型,所以,无需担心。

4.5.4 NaN

Number类型中,还有一个特殊的值叫NaN,意思是Not a Number不是一个数字。NaN最大的作用就是表示返回某个数值的操作失败了(⚠️注意,是失败,而不是错误)。举个简单的例子,0 / 0在数学运算中数不成立的,在其他语言中可能会报错,但是在JS中,这不会引发错误,而是得到NaN

💁 温馨提示

如果以0去除以任意数,会得到NaN,程序正常往后执行。如果其他数值进行符合现代数学运算规则的运算操作,超出边界之后会得到正负无穷值Infinity

  • 任何涉及到NaN的操作都会得到NaN
  • NaN不等于NaN,例如NaN == NaN会得到false

isNaN()

ECMA 提供了一个内置函数isNaN(),这个函数接受一个任意数据类型的参数。返回一个布尔值,表示传入值是不是NaN,就是是不是“不是一个数字”。注意点是,isNaN()在判断前,会把参数尝试着转为数值,再进行判断。下面是一些案例。

var a = 10;
var b = "10"
var c = "hello"
var d = ""
var boo = true;

isNaN(a) // false
isNaN(b) // false 转为数字后是10
isNaN(c) // true 因为‘hello’不能被转位数值
isNaN(d) // true 因为‘hello’不能被转位数值
isNaN(boo) // false 因为布尔值转为数值后为1,因此不是NaN,所以得到false

isNan()检测对象:

4.5.5 数值的转换

JS中,提供了三个内置函数用于将非数值转换为数值:

转换函数函数说明
Number()将任意数据类型转为数值
parseInt()将字符串类型转为整数
parseFloat()将字符串类型转为浮点数
  • Number()针对布尔值的处理:true转为1false转为0
  • Number()针对数值的处理:直接返回。
  • Number()针对null的处理:转为0
  • Number()针对undefined的处理:转为NaN
  • Number()针对字符串值的处理:
    • 非空字符串:不能包含非数字字符(除了加减号、以及正常小数点或科学计数法),均正常转换
    • 空字符串:转为0
    • 不满足上面的条件的字符串,均为NaN

上面列出了Nunber()针对常规的类型转换的大体规则,但是针对对象值的数值转换,相对特殊,这里着重说明:

在针对对象类型的值进行数值转换时,会先调用对象的valueOf()方法,再进行Number()转换,如果得到的结果是NaN,那么会再次调用对象的toString()方法得到一个字符串值,最终按照字符串转数字的规则进行转换。这里相对复杂,在了解对象后即可明白。valueOf()toString()是对象的原型方法,但是可以被自定义更改。

数值的转换规则比较多,而且繁杂,还涉及到其他进制。实际开发中,我们无需死记硬背,而且使用十进制居多,因此从现实使用角度理解即可,下面给出一些大概的规则结合使用案例就更容易理解。

Number(true) // 1
Number(false) // 0
Number('hello') // NaN
Number('1.1') // 1.1
Number('1.1.1') // NaN
Number(null) // 0
Number(undefined) // NaN

parseInt('1.1') // 1
parseInt('1.eqweq') // 1
parseFloat('1.1') // 1.1
parseFloat('1.1.1') // 1.1
parseFloat('1.1.1') // 1.1
parseFloat('1.dwadq') // 1
parseFloat('1.1.d') // 1.1
parseFloat('-00.1') // -0.1

parseInt()parseFloat()用于字符串转为数值的处理,如果传入数字值,实际上也会转为字符串再处理。

两个方法支持传入第二个参数,表示按某个进制进行解析。

parseInt()细节:字符串前面的空格会被忽略,从一个非空格字符串开始转换,如果第一个字符不是数值字符,加号或者减号,立即返回NaN,如果是数值符号,则继续解析,直到末尾(如果遇到非数值,则停止)

parseFloat()细节:同上类似,始终忽略字符串开头中的0,解析到末尾,或者第一个无效的的浮点数值

4.6 String类型

JS中,字符串通过单引号''或者双引号""包裹,使用哪一种都是一样的。需要注意如果通过单引号开头,就必须使用单引号结尾,使用双引号开头就必须使用双引号结尾。字符串中有一些比较有用的非打印字符,称转义字符,如下。

非打印字面量含义
\n换行
\t制表
\b退格
\r回车
\f换页
\反斜杠``
'单引号',用于单引号字符串中展示'内容
"双引号",用于单引号字符串中展示"内容
`反引号`
var str1 = 'hello,join' // 'hello,join'
var str2 = 'hello,'join'' // 'hello,'join''
4.6.1 字符串长度

字符串的长度通过length获取

var str = 'hello'
console.log(str.length) // 5
4.6.2 其他数据转为字符串

有两种方式可以把其他类型转为字符串:

  1. toString()

几乎所有的数据都有这个方法(除了nullundefined),该方法唯一的用途就是返回当前值的字符串形式。

var age = 11;
var ageString = age.toString() // '11'

var boo = true
var booString = boo.toString() // 'true'

toString()方法最常用的就是数值、布尔值、对象、字符串值,字符串值使用该方法就是返回自身的副本。

toString()方法不接收任何参数,不过对于数值调用的情况下,可以传入一个参数表示按什么进制转换。

  1. String()方法

除了toString()方法,可以使用内置的String()函数进行转换,String()接收一个参数,会返回参数变量对应的字符串表示,nullundefined均支持。

var n1 = null
var n2 = undefined
console.log(String(n1)) // 'null'
console.log(String(n2)) // 'undefined'

toString()String()的总结说明:

特性String()toString()
调用方式直接调用,是内置的全局函数作为对象的方法使用,定义在原型
适用范围任意类型的数据定义了toString()的类型
nullundefined支持,返回对应的字符串形式不支持,直接报错
语义层面强制转为字符串返回对象的字符串表示形式
4.6.3 模板字符串

ES6中,新增了模板字符串,不同于单双引号的字符串,模板字符串允许换行书写字符,但是需要注意的是:

  • 模板字符串会保留空格,因此在计算字符串长度时,会空格也算作长度。
  • 模板字符串在开始就换行,第一个字符实际上是换行符
var str = `hello,world`

var str1 = `
hello,
world
`
console.log(str1[0] === '\n'); // tru

模板插值

模板字符串最强大的特性的就是支持字符串插值,在技术层面来讲,模板字符串并不是纯粹的字符串,而是一个JS表达式,只不过表达式的返回值是一个字符串。模板字符串在定义时立即求值并转为字符串实例,任何插入的变量会从他们最近的作用域中取值。模板插值通过${}插入。

var name = '小红'
var str = `你好,${name}` // '你好,小红'

模板字符串中的${}插值的细节:

  • 插入的值会使用toString()强制转为字符串
  • 嵌套的模板字符串不需要转义处理
  • 在插值表达式中可以调用函数和方法
4.7 Symbol类型

符号(Symbol),是ES6新增的数据类型。符号是原始值,符号实例是唯一的、不可变的。用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。由于Symbol的特殊性,将在后面深入介绍了函数和对象后讲解。

var person = {
  name:'小红',
  age:20,
  address:['成都','北京'],
  start:function(){
    console.log('开始学习')
  }
}

字面量的对象抒写方式是比较推荐的,尤其是在初始化时需要设置大量数据的对象。


4.1.2. 对象的构成

对象由很多成员组成,每个成员都有自己的名字和一个值,简称key-value键值对。每组名字和值需要用逗号隔开,并且名字和值之间用冒号分隔,最后一组成员末尾不用加逗号,加了也不会引发错误(老式浏览器可能会)。

对象的成员的值可以是任意的,比如上面的person对象有四个成员,前三个个成员的值是字符串和数字和数组,最后一个成员的值是一个函数。前面三个称为数据项,也就是对象的属性。最后一个是函数,称为对象的方法。

var objName = {
  key1:value1,
  key2:value2,
  // .....
}

4.1.3. 方法简写

ECMA规范定义,如果对象的成员的值是函数,那么可以省略function,采用简写形式。

var obj = {
  start(){
    console.log('开始学习')
  }
}

4.1.4. 访问对象的属性和方法 - 点表示法

对象里面有很多属性和方法,访问它们的最常见的方式是点表示法。通过对象的名字打点调用对象的属性或者方法。对象的名字专业点来说是一个命名空间(namespace)。当我们访问对象的属性或者方法时,命名空间必须写在第一位,然后输入一个点。紧接着就是要访问的目标。

var obj = {
  name:'小红',
  start(){
    console.log('开始')
  },
  info:{
    address:'成都'
  }
}
console.log(obj.name) // 访问obj的name属性
console.log(obj.start()) // 访问obj的start方法
console.log(obj.info.address) // 访问obj下的info对象下的address

命名空间 namespace,实际上一种逻辑容器,用于对变量或函数进行分组隔离,防止命名冲突。作用就是让代码更加具有组织性和模块化,避免不同作用域中的变量相互干扰。此外,对象可以作为另一个对象的属性,称为子命名空间,访问时继续打点调用即可。


4.1.5. 访问对象的属性和方法 - 方括号表示法
var obj = {
  name:'小红',
  start(){
    console.log('开始')
  },
  info:{
    address:'成都'
  }
}
console.log(obj['name']) // 访问obj的name属性
console.log(obj['start']()) // 访问obj的start方法
console.log(obj['info']['address']) // 访问obj下的info对象下的address

可以看到方括号表示法,看起来像是访问一个数组元素:var arr = [1,2,3]`` arr[0] 。因此,有时候对象也称为关联数组。不同的是,数组是通过索引去访问成元素,而对象是通过字符串去映射某个属性值。

4.1.6. 设置对象成员

设置就是添加的意思,操作很简单,使用点表示法和方括号表示法均可,属性存在则是更新,不存在就是添加。

var person = {} // 通过字面量创建一个空对象

person.name = '小明'
person['info']['address'] = '成都'
person['start'] = function () {
  console.log('开始...')
}

4.1.7. 关键字 this

作为JS中的难点,this一定是很多开发者的噩梦。它是一个特殊的关键字,表示的是当前执行上下文中的上下文对象。因为这涉及到代码底层的运行机制。后面将会深入讲解。

在对象方法中,使用this是非常常见的。这里仅给出一个简单示例。

var person= {
  name:'小明',
  getName(){
    console.log(this.name) // 此处的this指的是person这个对象
  }
}
person.getName() // '小明'

给出这个示例的原因是,下面我们将要批量创建对象,因为手动创建,效率太低了,而批量创建,this是关键。


4.1.8. 构造函数

手工创建对象是为了便于我们理解对象,实际开发中,手动创建是低效的。举个例子,我们要创建10个对象,这10个对象都拥有name属性和start方法。此时唯有使用函数批量创建才能解决效率问题。

// 定义创建对象的函数 传入name参数,每次调用该函数 返回一个对象
// 对象具有两个成员:一个name属性 一个start方法
function createPerson(name){
  var obj = {};
  obj.name = name;
  obj.start = function(){
    console.log(`我是${this.name}`)
  }
  return obj;
}

var obj1 = createPerson('小明');
var obj2 = createPerson('小刚');
obj1.start() // '我是小明'
obj2.start() // '我是小刚'

上面这个案例,并不是构造函数,只是一个普通函数。我们在函数内部通过字面量来创建新对象,并显示的返回对象实例。代码稍显冗余,因为我们必须要创建一个空对象才能进行返回。虽然逻辑上来说没有任何问题,但是最优的方法是使用构造函数。同时,上面这个案例使用了this。在start函数中通过this访问name属性,我们创建了两个对象:obj1obj2,两个对象的start方法分别输出了各自的name属性,因为程序运行时上下文中的对象改变了。这只是为了使用this而演示,实际上start中不使用this也会输出对应的name,因为name是函数参数,站在函数的角度上,直接使用name是没有问题的,下面介绍构造函数。


构造函数使用new关键字调用函数。当我们使用new时,JS引擎将会进行如下操作:

  • 创建一个新对象
  • this绑定到新对象,以便在构造函数体代码中使用this
  • 运行构造函数代码
  • 返回新对象
  • 因此无需我们手动抒写多余代码,这个步骤,在讲完原型后,我们会深入解析

构造函数首字母大写,这是社区约定惯例,属于最佳实践,并不是ECMA规范,看如下案例。

function Person (name) {
  this.name = name
  this.start = function () {
    console.log(`我是${this.name}`)
  }
}
var obj1 = new Person('小红')

5. 流程控制语句

ECMA规范,提供了很多语句用于控制代码走向,称为流程控制语句。

5.1 if 语句

if语句在所有编程语言中都是使用最频繁的语句,语法如下:

if(condition) statement1 else statement2
  • condition
    • 表示条件,可以是任何表达式,因此表达式的结果也可以是任意的。ECMA会自动调用Boolean(),将这个表达式转为布尔值。如果布尔值为真值,则执行statement1,反之则执行statement2
  • statement:
    • 表示任何有效的JavaScript语句
var a = 'hello'

if(a == 'hello'){
  console.log('a是hello')
}else{
  console.log('a不是hello')
}

连续的if语句else if 。语法如下:

if (condition1) statement1 else if (condition2) statement2 else statement3
var num = 20;

if(num > 20) {
  console.log('num大于20')
}else if (num === 20) {
  console.log('num等于20')
}else{
  // some code ...
}

三元运算符,是if else 的简写形式,因为用到三个操作数,固称三元运算符,语法如下:

variable = condition ? true_value : false_value
  • variable:表示变量,它的值要么是true_value,要么是false_value,取决于condition
  • condition:表达式在底层会被自动转换为布尔值。值为真,则将true_value赋值给变量,值为假,则赋值false_value给变量。
var str = 'hello'

var isTrueStr = str.length ? true : false

console.log(isTrueStr) // true 因为str的长度是个正整数
5.2 循环语句

循环,直白点来说就是重复。在程序中,循环是一种能够重复执行的特定的代码块。通常这个代码块根据某种条件来决定循环是否继续。

循环的组成:

  • 初始化:设置循环开始时的初始状态
  • 条件判断:决定循环是否继续执行的条件
  • 循环体:要重复执行的代码
  • 更新操作:每次循环迭代后更新变量的状态,使得循环逐步达到终止条件
5.2.1 do-while 语句

do-while是一种后测试的循环语句,后测试就是值循环体先不管条件,先执行一次,再介入条件判断。

do { 
 statement 
} while (expression)
do {
    console.log(true)
} while (false)

// 虽然条件是false,但是至少要执行一次
let i = 0;

do {
  i += 2
  console.log(i) // 2 4 6 8 10
} while (i < 10)
// 只要 i 小于 10,循环就会重复执行。i0 开始,每次循环递增 2
5.2.2 while 语句

while是一种先测试循环语句,即先检测退出条件,再执行循环体内代码。因此while循环体内的代码可能不执行,因为条件可能不满足。

while(expression) statement
let i = 0;
while(i < 10){
  i += 2
  console.log(i) // 2 4 6 8 10
}
// 变量 i0 开始,每次循环递增 2。只要 i 小于 10,循环就会继续,否则停止
5.2.3 for 循环语句

while一样,for语句 也是先测试语句,区别是for循环的语法结构中就包含了进入循环之前的初始化代码、以及循环执行后要执行的表达式。这个结构相对复杂,同时也是实际开发中使用得对多的循环语法。

for (initialization;expression;post-loop-expression) statement
  • initialization:初始化状态
  • expression:条件判断表达式
  • post-loop-expression:更新操作,也叫后循环或者循环后表达式
  • statement:循环体代码
for (var i = 0; i < 5; i++){
  console.log(i) // 0 1 2 3 4
}

while循环for循环不同仅限于语法结构层面,因此能通过while循环的代码也能通过for循环


for循环具有非常高的灵活性,除了循环体外的部分,初始化条件判断更新操作均可以省略。

for(;;){
  // some code ... 无限循环
}

省略后就是一个死循环也叫无限循环,需要注意的是,省略循循环组成的表达式,不能省略分号;``;


省略还有一层意思,就是语法层面的简化,可以不必按照语法规则。初始化表达式可以提取到for外面,条件更新也可以写到循环里面。如下:

var i = 0;
for(; i < 10;){
  i++
  console.log(i)
}

无限循环并非没有用途,在一些高级的场景中,会使用到,比如浏览器事件循环机制等,但是,使用无限循环,必须要给定退出循环的条件,否则导致程序死循环,占用大量CPU资源,导致页面、甚至服务器崩溃。

下面只是一个简单示例,不做过多深入,因为实际开发中,几乎不会用到。

for(; ;){
  if(someCondition){
    throw new Error('')
  }
}
5.2.4 for-in 和 for of 语句

for - in语句是一种严格迭代语句,用于枚举对象中的非符号键属性 - 待定(讲完对象再讲)

5.2.5 break和continue 语句

breakcontinue语句,主要是为执行循环代码提供了两种更为严格的控制手段。

  • break: 用于立即退出循环,退出的是整个循环。
  • continue: 用于立即退出循环,退出的是当前次循环,然后又从循环顶部开始执行。
let num = 0;
for(var i = 1; i < 10; i++){
  if(i % 5 == 0) {
    break;
  }
  num++
}
console.log(num) // 4

上面这个案例中,num输出为4,我们来拆解一下:

  1. 首次循环,然后判断 => 1 % 5 得到false,不进入判断执行break,执行num++ num1
  2. 二次循环,然后判断 => 2 % 5 得到false,不进入判断执行break,执行num++ num2
  3. 三次循环,然后判断 => 3 % 5 得到false,不进入判断执行break,执行num++ num3
  4. 四次循环,然后判断 => 4 % 5 得到false,不进入判断执行break,执行num++ num4
  5. 五次循环,然后判断 => 5 % 5 得到true,进入判断执行break,跳出整个循环,num最终为4

var num = 0;
for(var i = 1; i < 10; i++){
  if(i % 5 == 0){
    continue
  }
  num++
}
console.log(num) // 8

上面这个案例中,num输出8:

正常情况下,循环要执行9次 分别是1 - 9,num也会是9,但是i等于5时会执行continue,终止该轮循环,从而导致该轮的num++不执行。最终num输出8

5.2.6 标签语句

标签语句用于给语句添加标签,语法很简单:

labelName: statement

标签语句在JS中,不是特别常用,一般来说主要用于循环语句,其他代码块也可使用。作为了解即可。

outerLoop: for (var i = 0; i < 3; i++) {
    for (var j = 0; j < 3; j++) {
        if (i === 1 && j === 1) {
            break outerLoop; // 跳出 outerLoop 的循环
        }
        console.log(`i=${i}, j=${j}`);
    }
}

上面这个案例,break用于指定跳出outerLoop循环,而不仅仅是内层循环


myBlock: {
    console.log("This is the first statement.");
    if (true) {
        break myBlock; // 提前退出代码块
    }
    console.log("This will not be executed.");
}
console.log("Code block exited.");
5.2.7 with 语句

with 语句的作用是将代码作用于设置为特定的对象,语法如下:

width (expression) statement;
var qs = location.search.substring(1); 
var hostName = location.hostname; 
var url = location.href;
with(location){
 var qs = search.substring(1);
 var hostName = hostname; 
 var url = href;
}

with会严重影响JS性能,并且难以调试,因此不推荐使用,了解即可。

5.2.8 switch 语句

switch语句if语句差不多,都是用于条件判断的流程控制语句,和C语言非常类似。语法如下:

switcH (expression){
  case value1:
    statement
    break;
  case value2:
    statement
    break;
  case value3:
    statement
    break;
  default: 
    statemen
}
  • expression:判断条件
  • case: 判断分支
  • break: 跳出判断
  • default: 条件不满足时执行,相当于else
if( i == 25){
  console.log(25)
}else if (i == 35) {
   console.log(35)
}else if (i == 45) {
   console.log(45)
}else{
  console.log(100)
}
switch(i){
  case 25:
    console.log(35)
    break;
  case 35:
    console.log(35)
    break;
  case 45:
    console.log(45)
    break;
  default:
    console.log('其他')
}

switch case穿透

var value = 2;

switch (value) {
  case 1:
    console.log("Case 1");
  case 2:
    console.log("Case 2");
  case 3:
    console.log("Case 3");
  default:
    console.log("Default case");
}

// Case 2
// Case 3
// Default case

如上代码,在使用switch时,如果不给case子句加break那么代码就会继续执行,这种行为称为穿透。

有时候,利用这个特性,可以简化代码。

var value = "A";

switch (value) {
  case "A":
  case "B":
  case "C":
    console.log("Matched A, B, or C");
    break;
  case "D":
    console.log("Matched D");
    break;
  default:
    console.log("No match");
}
// Matched A, B, or C

switch可以优化if的结构,但是要注意switch的条件判断是采用全等判断,不会发生类型转换。