目录
js面向对象编程是什么?
面向对象编程(Object-Oriented Programming,简称OOP)是一种编程范式,它将程序中的数据和操作数据的方法封装在一起,形成对象。每个对象都有自己的属性和行为,可以与其他对象进行交互和协作完成任务。
面向对象编程主要包括以下几个概念:
-
类(Class):类是一种抽象的模板或蓝图,用来描述具有相同属性和方法的对象集合。它定义了对象的状态和行为。
-
对象(Object):对象是类的实例,在内存中分配空间后才能被使用。它具有类定义的属性和方法。
-
继承(Inheritance):继承是指子类可以继承父类的属性和方法,并且可以添加自己的属性和方法。
-
多态(Polymorphism):多态是指同一类型的对象可以具有不同的形态。在面向对象编程中,多态通常通过重载和重写来实现。
-
封装(Encapsulation):封装是指隐藏对象的内部实现细节,只暴露必要的接口供外部访问。这样可以保证对象的安全性和可靠性。
面向对象编程可以使代码更加清晰、易于维护和扩展,同时也提高了代码的复用性和可读性。它在现代软件开发中得到了广泛的应用,包括桌面应用、Web应用和移动应用等。
JavaScript 的基本语法
语句
在计算机语言中,执行特定任务的一组单词、数字和运算符被称为语句。
语句是为了完成某种任务而进行的操作,比如下面就是一行赋值语句。
var a = 1 + 3;
这条语句先用var
命令,声明了变量a
,然后将1 + 3
的运算结果赋值给变量a
。
相比之下,1或者3本身就是一个值,称为字面量。
1 + 3
叫做表达式,指一个为了得到返回值的计算式。语句和表达式的区别在于,前者主要为了进行某种操作,一般情况下不需要返回值;后者则是为了得到返回值,一定会返回一个值。凡是 JavaScript 语言中预期为值的地方,都可以使用表达式。比如,赋值语句的等号右边,预期是一个值,因此可以放置各种表达式。
语句以分号结尾,一个分号就表示一个语句结束。多个语句可以写在一行内。
var a = 1 + 3 ; var b = 'abc';
分号前面可以没有任何内容,JavaScript 引擎将其视为空语句。
;;;
上面的代码就表示3个空语句。
表达式不需要分号结尾。一旦在表达式后面添加分号,则 JavaScript 引擎就将表达式视为语句,这样会产生一些没有任何意义的语句。
1 + 3; 'abc';
上面两行语句只是单纯地产生一个值,并没有任何实际的意义。
区分大小写
JavaScript 是区分大小写的。也就是说,关键字、变量、函数名和所有的标识符(identifier)都必须采取一致的大小写形式。
比如,关键字 while
必须写成 while
,而不能写成 While
或者 WHILE
。同样,online
、Online
、OnLine
、ONLINE
是4个不同的
变量名。
但需要注意的是,HTML 并不区分大小写。由于它和客户端 JavaScript 联系紧密,因此这点区别很容易混淆。许多客户端
JavaScript 对象和属性与他们所表示的 HTML 标签和属性名相同。在 HTML 中,这些标签和属性名可以使用大写也可以是小
写,而在 JavaScript 中则必须是小写。例如,在 HTML 中设置事件处理程序时,onclick
属性可以写成 onClick
,但在
JavaScript 代码中,必须使用小写的 onclick
。
var userName = '张三'; var username = '李四'; console.log(userName); console.log(username)
标识符
标识符指的是用来识别各种值的合法名称。最常见的标识符就是变量名,以及后面要提到的函数名。JavaScript 语言的标识符对大小写敏感,所以a
和A
是两个不同的标识符。
标识符有一套命名规则,不符合规则的就是非法标识符。JavaScript 引擎遇到非法标识符,就会报错。
简单说,标识符命名规则如下。
-
第一个字符,可以是任意 Unicode 字母(包括英文字母和其他语言的字母),以及美元符号(
$
)和下划线(_
)。 -
第二个字符及后面的字符,除了 Unicode 字母、美元符号和下划线,还可以用数字
0-9
。
注意事项
按照惯例,ECMAScript标识符使用驼峰法大小写形式;
下面这些都是合法的标识符。
arg0 _tmp $elem π
下面这些则是不合法的标识符。
1a // 第一个字符不能是数字 23 // 同上 *** // 标识符不能包含星号 a+b // 标识符不能包含加号 -d // 标识符不能包含减号或连词线
中文是合法的标识符,可以用作变量名。
var 临时变量 = 1;
JavaScript 有一些保留字,不能用作标识符:arguments、break、case、catch、class、const、continue、debugger、default、delete、do、else、enum、eval、export、extends、false、finally、for、function、if、implements、import、in、instanceof、interface、let、new、null、package、private、protected、public、return、static、super、switch、this、throw、true、try、typeof、var、void、while、with、yield。
注释
源码中被 JavaScript 引擎忽略的部分就叫做注释,它的作用是对代码进行解释。Javascript 提供两种注释的写法:一种是单行注释,用//
起头;另一种是多行注释,放在/*
和*/
之间。
// 这是单行注释 /* 这是 多行 注释 */
区块
JavaScript 使用大括号,将多个相关的语句组合在一起,称为“区块”(block)。
对于var
命令来说,JavaScript 的区块不构成单独的作用域(scope)。
{ var a = 1; } a // 1
上面代码在区块内部,使用var
命令声明并赋值了变量a
,然后在区块外部,变量a
依然有效,区块对于var
命令不构成单独的作用域,与不使用区块的情况没有任何区别。在 JavaScript 语言中,单独使用区块并不常见,区块往往用来构成其他更复杂的语法结构,比如for
、if
、while
、function
等。
严格模式
ES5增加了严格模式。严格模式是一种不同的Js解析和执行模型,ES3的一些不规范写法在严格模式下不会被处理,对于不安全的活动将抛出错误。
定义严格模式,需要在代码开头写上“use strict”;
也可以在指定的函数中使用严格模式,例如
function doSomeThink(){ "use strict"; }
区别
1、在严格模式下创建变量
在非严格模式下,可以直接声明一个全局变量,而不是用 var、let 或 const 关键字,并且还不报错。示例:
a = 1;
果是在严格模式下,以上面方式声明变量,则不被允许,会在执行代码是抛出ReferenceError
。 并且在严格模式下,不允许在变量上调用 delete,这也会导致ReferenceError
异常,如果在普通模式下,即使失败也是静默返回一个 false。
let a = 1; delete a;
2、在严格模式下的函数
命名函数的参数必须唯一
//重名参数 //非严格模式:没有错误,只能访问第二个参数 //严格模式:抛出语法错误 function sum (num, num){ //do something }
变量
概念
变量是对“值”的具名引用。变量就是为“值”起名,然后引用这个名字,就等同于引用这个值。变量的名字就是变量名。
var a = 1;
上面的代码先声明变量a
,然后在变量a
与数值1之间建立引用关系,称为将数值1“赋值”给变量a
。以后,引用变量名a
就会得到数值1。最前面的var
,是变量声明命令。它表示通知解释引擎,要创建一个变量a
。
注意,JavaScript 的变量名区分大小写,A
和a
是两个不同的变量。
变量的声明和赋值,是分开的两个步骤,上面的代码将它们合在了一起,实际的步骤是下面这样。
var a; a = 1;
如果只是声明变量而没有赋值,则该变量的值是undefined
。undefined
是一个特殊的值,表示“无定义”。
var a; a // undefined
如果变量赋值的时候,忘了写var
命令,这条语句也是有效的。
var a = 1; // 基本等同 a = 1;
但是,不写var
的做法,不利于表达意图,而且容易不知不觉地创建全局变量,所以建议总是使用var
命令声明变量。
如果一个变量没有声明就直接使用,JavaScript 会报错,告诉你变量未定义。
x // ReferenceError: x is not defined
上面代码直接使用变量x
,系统就报错,告诉你变量x
没有声明。
可以在同一条var
命令中声明多个变量,多个变量使用逗号分隔开。
var a, b;
JavaScript 是一种动态类型语言,也就是说,变量的类型没有限制,变量可以随时更改类型。
var a = 1; a = 'hello';
上面代码中,变量a
起先被赋值为一个数值,后来又被重新赋值为一个字符串。第二次赋值的时候,因为变量a
已经存在,所以不需要使用var
命令。
如果使用var
重新声明一个已经存在的变量,是无效的。
var x = 1; var x; x // 1
上面代码中,变量x
声明了两次,第二次声明是无效的。
但是,如果第二次声明的时候还进行了赋值,则会覆盖掉前面的值。
var x = 1; var x = 2; // 等同于 var x = 1; var x; x = 2;
作用域
使用var操作符定义的变量会成为包含它的函数的局部变量。
例如:
使用var在一个函数内部定义一个变量,意味着函数在退出时变量将会被销毁。
function test(){ var message = "H1"; }
去掉var操作符后,message就变成了全局变量,只要调用一次函数test(),就会定义这个变量,并且可以在函数外面访问到
function test(){ message = "H1"; }
定义多个变量
如果需要定义多个变量,可以使用逗号分隔开
例如:
var name='张三',age=20;
变量提升
JavaScript 引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升。
console.log(a); var a = 1;
上面代码首先使用console.log
方法,在控制台(console)显示变量a
的值。这时变量a
还没有声明和赋值,所以这是一种错误的做法,但是实际上不会报错。因为存在变量提升,真正运行的是下面的代码。
var a; console.log(a); a = 1;
最后的结果是显示undefined
,表示变量a
已声明,但还未赋值。
let声明
let跟var的作用差不多,但有着非常重要区别,最重要的区别是,let声明的范围是块作用域,而var声明的范围是函数的作用域。
if(true){ var name = 'Matt'; console.log(name);//Matt } console.log(name);//Matt if(true){ let name = 'Matt'; console.log(name);//Matt } console.log(name);//name未定义
let不允许在一个块作用域中出现冗余声明,这样会导致报错:
var name; var name; let age; let age;//标识符age已经声明过了
暂时性死区
let声明的变量不会在作用域中被提升
console.log(name) var name = 'Matt'; console.log(age) let age = 26;
全局声明
JS中声明全局变量主要分为显式声明或者隐式声明下面分别介绍。
声明方式一:
使用var(关键字)+变量名(标识符) 的方式在function外部声明,即为全局变量,否则在function声明的是局部变量。该方式即
为显式声明详细如下:
var test = 5;//全局变量 function a(){ var a = 3;//局部变量 alert(a); } function b(){ alert(test); } //a();//调用a方法,那么方法里面的内容才会执行 //b();//同上
声明方式二:
没有使用var,直接给标识符test赋值,这样会隐式的声明了全局变量test。即使该语句是在一个function内,当该function被执行
后test变成了全局变量。
test = 5;//全局变量 function a(){ aa = 3;//也是全局变量 alert(test); } //a(); //输出5 //alert(aa);//这里也可以方法a()方法里面的变量,因为aa是全局变量
声明方式三:
使用window全局对象来声明,全局对象的属性对应也是全局变量,详细如下:
window.test = 50; alert(test);//输出50
for循环中let的声明
在let出现之前,for循环定义的迭代变量会渗透到循环体外部:
for(var i = 0;i<5;i++){ } console.log(i)//5
改使用let后,这个问题就消失了,因为迭代变量的作用域仅限于for循环块内部
for(let i = 0;i<5;i++){ } console.log(i)
const声明
const的行为与let基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且尝试修改const声明的变量会导致运行时错误。
const age = 29; age = 36;
const声明的限制只适用于它指向的变量的引用。如果const变量引用的是一个对象,那么修改这个对象内部的属性并不违反const的限制。
const person = {}; person.name = "振涛教育";
条件语句
JavaScript 提供if
结构和switch
结构,完成条件判断,即只有满足预设的条件,才会执行相应的语句。
if 结构
if
结构先判断一个表达式的布尔值,然后根据布尔值的真伪,执行不同的语句。所谓布尔值,指的是 JavaScript 的两个特殊值,true
表示真,false
表示伪
。
if (布尔值) 语句; // 或者 if (布尔值) 语句;
上面是if
结构的基本形式。需要注意的是,“布尔值”往往由一个条件表达式产生的,必须放在圆括号中,表示对表达式求值。如果表达式的求值结果为true
,就执行紧跟在后面的语句;如果结果为false
,则跳过紧跟在后面的语句。
if (m === 3) m = m + 1;
上面代码表示,只有在m
等于3时,才会将其值加上1。
这种写法要求条件表达式后面只能有一个语句。如果想执行多个语句,必须在if
的条件判断之后,加上大括号,表示代码块(多个语句合并成一个语句)。
if (m === 3) { m += 1; }
建议总是在if
语句中使用大括号,因为这样方便插入语句。
注意,if
后面的表达式之中,不要混淆赋值表达式(=
)、严格相等运算符(===
)和相等运算符(==
)。尤其是赋值表达式不具有比较作用。
var x = 1; var y = 2; if (x = y) { console.log(x); } // "2"
上面代码的原意是,当x
等于y
的时候,才执行相关语句。但是,不小心将严格相等运算符写成赋值表达式,结果变成了将y
赋值给变量x
,再判断变量x
的值(等于2)的布尔值(结果为true
)。
这种错误可以正常生成一个布尔值,因而不会报错。为了避免这种情况,有些开发者习惯将常量写在运算符的左边,这样的话,一旦不小心将相等运算符写成赋值运算符,就会报错,因为常量不能被赋值。
if (x = 2) { // 不报错 if (2 = x) { // 报错
至于为什么优先采用“严格相等运算符”(===
),而不是“相等运算符”(==
),请参考《运算符》章节。
if...else 结构
if
代码块后面,还可以跟一个else
代码块,表示不满足条件时,所要执行的代码。
if (m === 3) { // 满足条件时,执行的语句 } else { // 不满足条件时,执行的语句 }
上面代码判断变量m
是否等于3,如果等于就执行if
代码块,否则执行else
代码块。
对同一个变量进行多次判断时,多个if...else
语句可以连写在一起。
if (m === 0) { // ... } else if (m === 1) { // ... } else if (m === 2) { // ... } else { // ... }
else
代码块总是与离自己最近的那个if
语句配对。
var m = 1; var n = 2; if (m !== 1) if (n === 2) console.log('hello'); else console.log('world');
上面代码不会有任何输出,else
代码块不会得到执行,因为它跟着的是最近的那个if
语句,相当于下面这样。
if (m !== 1) { if (n === 2) { console.log('hello'); } else { console.log('world'); } }
如果想让else
代码块跟随最上面的那个if
语句,就要改变大括号的位置。
if (m !== 1) { if (n === 2) { console.log('hello'); } } else { console.log('world'); } // world
案例练习:
1、企业发放的奖金根据利润提成。 利润(I)低于或等于10万元时,奖金可提10%; 利润高于10万元,低于20万元时,低于10万元的部分按10%提成,高于10万元的部分,可提成7.5%; 20万到40万之间时,高于20万元的部分,可提成5%; 40万到60万之间时高于40万元的部分,可提成3%; 60万到100万之间时,高于60万元的部分,可提成1.5%; 高于100万元时,超过100万元的部分按1%提成。 2、输入某年某月某日,判断这一天是这一年的第几天?
switch 结构
多个if...else
连在一起使用的时候,可以转为使用更方便的switch
结构。
switch (fruit) { case "banana": // ... break; case "apple": // ... break; default: // ... }
上面代码根据变量fruit
的值,选择执行相应的case
。如果所有case
都不符合,则执行最后的default
部分。需要注意的是,每个case
代码块内部的break
语句不能少,否则会接下去执行下一个case
代码块,而不是跳出switch
结构。
var x = 1; switch (x) { case 1: console.log('x 等于1'); case 2: console.log('x 等于2'); default: console.log('x 等于其他值'); } // x等于1 // x等于2 // x等于其他值
上面代码中,case
代码块之中没有break
语句,导致不会跳出switch
结构,而会一直执行下去。正确的写法是像下面这样。
switch (x) { case 1: console.log('x 等于1'); break; case 2: console.log('x 等于2'); break; default: console.log('x 等于其他值'); }
switch
语句部分和case
语句部分,都可以使用表达式。
switch (1 + 3) { case 2 + 2: f(); break; default: neverHappens(); }
上面代码的default
部分,是永远不会执行到的。
需要注意的是,switch
语句后面的表达式,与case
语句后面的表示式比较运行结果时,采用的是严格相等运算符(===
),而不是相等运算符(==
),这意味着比较时不会发生类型转换。
var x = 1; switch (x) { case true: console.log('x 发生类型转换'); break; default: console.log('x 没有发生类型转换'); } // x 没有发生类型转换
上面代码中,由于变量x
没有发生类型转换,所以不会执行case true
的情况。这表明,switch
语句内部采用的是“严格相等运算符”,详细解释请参考《运算符》一节。
三元运算符 ?:
JavaScript 还有一个三元运算符(即该运算符需要三个运算子)?:
,也可以用于逻辑判断。
(条件) ? 表达式1 : 表达式2
上面代码中,如果“条件”为true
,则返回“表达式1”的值,否则返回“表达式2”的值。
var even = (n % 2 === 0) ? true : false;
上面代码中,如果n
可以被2整除,则even
等于true
,否则等于false
。它等同于下面的形式。
var even; if (n % 2 === 0) { even = true; } else { even = false; }
这个三元运算符可以被视为if...else...
的简写形式,因此可以用于多种场合。
var myVar; console.log( myVar ? 'myVar has a value' : 'myVar does not have a value' ) // myVar does not have a value
上面代码利用三元运算符,输出相应的提示。
var msg = '数字' + n + '是' + ( n % 2 === 0 ? '偶数' : '奇数');
上面代码利用三元运算符,在字符串之中插入不同的值。
循环语句
循环语句用于重复执行某个操作,它有多种形式。
while 循环
While
语句包括一个循环条件和一段代码块,只要条件为真,就不断循环执行代码块。
while (条件) 语句; // 或者 while (条件) 语句;
while
语句的循环条件是一个表达式,必须放在圆括号中。代码块部分,如果只有一条语句,可以省略大括号,否则就必须加上大括号。
while (条件) { 语句; }
下面是while
语句的一个例子。
var i = 0; while (i < 100) { console.log('i 当前为:' + i); i = i + 1; }
上面的代码将循环100次,直到i
等于100为止。
下面的例子是一个无限循环,因为循环条件总是为真。
while (true) { console.log('Hello, world'); }
for 循环
for
语句是循环命令的另一种形式,可以指定循环的起点、终点和终止条件。它的格式如下。
for (初始化表达式; 条件表达式; 操作表达式) 语句 // 或者 for (初始化表达式; 条件表达式; 操作表达式) { 语句 }
for
语句后面的括号里面,有三个表达式。
-
初始化表达式(initialize):确定循环变量的初始值,只在循环开始时执行一次。
-
条件表达式(test):每轮循环开始时,都要执行这个条件表达式,只有值为真,才继续进行循环。
-
操作表达式(increment):每轮循环的最后一个操作,通常用来递增循环变量。
下面是一个例子。
var x = 3; for (var i = 0; i < x; i++) { console.log(i); } // 0 // 1 // 2
上面代码中,初始化表达式是var i = 0
,即初始化一个变量i
;测试表达式是i < x
,即只要i
小于x
,就会执行循环;递增表达式是i++
,即每次循环结束后,i
增大1。
所有for
循环,都可以改写成while
循环。上面的例子改为while
循环,代码如下。
var x = 3; var i = 0; while (i < x) { console.log(i); i++; }
for
语句的三个部分(initialize、test、increment),可以省略任何一个,也可以全部省略。
for ( ; ; ){ console.log('Hello World'); }
上面代码省略了for
语句表达式的三个部分,结果就导致了一个无限循环。
案例练习:
1、有1、2、3、4个数字,能组成多少个互不相同且无重复数字的三位数?都是多少? for(var i = 1;i<5;i++){ for(var h = 1;h<5;h++){ for(var j = 1;j<5;j++){ if(i != h && i!=j && j!=h){ console.log(i+":"+h+":"+j) } } } } 2、输出9*9口诀。
do...while 循环
do...while
循环与while
循环类似,唯一的区别就是先运行一次循环体,然后判断循环条件。
do 语句 while (条件); // 或者 do { 语句 } while (条件);
不管条件是否为真,do...while
循环至少运行一次,这是这种结构最大的特点。另外,while
语句后面的分号注意不要省略。
下面是一个例子。
var x = 3; var i = 0; do { console.log(i); i++; } while(i < x);
for-in语句
for-in语句是一种严格的迭代语句,用于枚举对象中的非符号键属性,语法如下
for(属性 in 表达式){ 语句 }
例如:
for(const propName in window){ console.log(propName) }
for-of语句
for of
遍历的是数组元素值,而且for of
遍历的只是数组内的元素,不包括原型属性和索引。
var arr = [1,2,3] arr.a = 123 Array.prototype.a = 123 for (let value of arr) { console.log(value) }
for of
适用遍历数/数组对象/字符串/map
/set
等拥有迭代器对象(iterator
)的集合,但是不能遍历对象,因为没有迭代器对象,但如果想遍历对象的属性,你可以用for in
循环(这也是它的本职工作)或用内建的Object.keys()
方法
var myObject={ a:1, b:2, c:3 } for (var key of Object.keys(myObject)) { console.log(key + ": " + myObject[key]); }
break 语句和 continue 语句
break
语句和continue
语句都具有跳转作用,可以让代码不按既有的顺序执行。
break
语句用于跳出代码块或循环。
var i = 0; while(i < 100) { console.log('i 当前为:' + i); i++; if (i === 10) break; }
上面代码只会执行10次循环,一旦i
等于10,就会跳出循环。
for
循环也可以使用break
语句跳出循环。
for (var i = 0; i < 5; i++) { console.log(i); if (i === 3) break; } // 0 // 1 // 2 // 3
上面代码执行到i
等于3,就会跳出循环。
continue
语句用于立即终止本轮循环,返回循环结构的头部,开始下一轮循环。
var i = 0; while (i < 100){ i++; if (i % 2 === 0) continue; console.log('i 当前为:' + i); }
上面代码只有在i
为奇数时,才会输出i
的值。如果i
为偶数,则直接进入下一轮循环。
如果存在多重循环,不带参数的break
语句和continue
语句都只针对最内层循环。
标签(label)
JavaScript 语言允许,语句的前面有标签(label),相当于定位符,用于跳转到程序的任意位置,标签的格式如下。
label: 语句
标签可以是任意的标识符,但不能是保留字,语句部分可以是任意语句。
标签通常与break
语句和continue
语句配合使用,跳出特定的循环。
top: for (var i = 0; i < 3; i++){ for (var j = 0; j < 3; j++){ if (i === 1 && j === 1) break top; console.log('i=' + i + ', j=' + j); } } // i=0, j=0 // i=0, j=1 // i=0, j=2 // i=1, j=0
上面代码为一个双重循环区块,break
命令后面加上了top
标签(注意,top
不用加引号),满足条件时,直接跳出双层循环。如果break
语句后面不使用标签,则只能跳出内层循环,进入下一次的外层循环。
标签也可以用于跳出代码块。
foo: { console.log(1); break foo; console.log('本行不会输出'); } console.log(2); // 1 // 2
上面代码执行到break foo
,就会跳出区块。
continue
语句也可以与标签配合使用。
top: for (var i = 0; i < 3; i++){ for (var j = 0; j < 3; j++){ if (i === 1 && j === 1) continue top; console.log('i=' + i + ', j=' + j); } } // i=0, j=0 // i=0, j=1 // i=0, j=2 // i=1, j=0 // i=2, j=0 // i=2, j=1 // i=2, j=2
上面代码中,continue
命令后面有一个标签名,满足条件时,会跳过当前循环,直接进入下一轮外层循环。如果continue
语句后面不使用标签,则只能进入下一轮的内层循环。
数据类型概述
简介
JavaScript 语言的每一个值,都属于某一种数据类型。JavaScript 的数据类型,共有七种。
-
数值(number):整数和小数(比如
1
和3.14
) -
字符串(string):文本(比如
Hello World
)。 -
布尔值(boolean):表示真伪的两个特殊值,即
true
(真)和false
(假) -
undefined
:表示“未定义”或不存在,即由于目前没有定义,所以此处暂时没有任何值 -
null
:表示空值,即此处的值为空。 -
对象(object):各种值组成的集合。
-
符号(symbol,Es6中新增)
通常,数值、字符串、布尔值这三种类型,合称为原始类型的值,即它们是最基本的数据类型,不能再细分了。对象则称为合成类型的值,因为一个对象往往是多个原始类型的值的合成,可以看作是一个存放各种值的容器。至于undefined
和null
,一般将它们看成两个特殊值。
对象是最复杂的数据类型,又可以分成三个子类型。
-
狭义的对象(object)
-
数组(array)
-
函数(function)
狭义的对象和数组是两种不同的数据组合方式,除非特别声明,本教程的”对象“都特指狭义的对象。函数其实是处理数据的方法,JavaScript 把它当成一种数据类型,可以赋值给变量,这为编程带来了很大的灵活性,也为 JavaScript 的“函数式编程”奠定了基础。
typeof 运算符
typeof
运算符可以返回一个值的数据类型。
数值、字符串、布尔值分别返回number
、string
、boolean
。
typeof 123 // "number" typeof '123' // "string" typeof false // "boolean"
函数返回function
。
function f() {} typeof f // "function"
undefined
返回undefined
。
typeof undefined // "undefined"
利用这一点,typeof
可以用来检查一个没有声明的变量,而不报错。
v // ReferenceError: v is not defined typeof v // "undefined"
上面代码中,变量v
没有用var
命令声明,直接使用就会报错。但是,放在typeof
后面,就不报错了,而是返回undefined
。
实际编程中,这个特点通常用在判断语句。
// 错误的写法 if (v) { // ... } // ReferenceError: v is not defined // 正确的写法 if (typeof v === "undefined") { // ... }
对象返回object
。
typeof window // "object" typeof {} // "object" typeof [] // "object"
上面代码中,空数组([]
)的类型也是object
,这表示在 JavaScript 内部,数组本质上只是一种特殊的对象。
var o = {}; var a = []; o instanceof Array // false a instanceof Array // true
null
返回object
。
typeof null // "object"
null
的类型是object
,这是由于历史原因造成的。1995年的 JavaScript 语言第一版,只设计了五种数据类型(对象、整数、浮点数、字符串和布尔值),没考虑null
,只把它当作object
的一种特殊值。后来null
独立出来,作为一种单独的数据类型,为了兼容以前的代码,typeof null
返回object
就没法改变了。
instanceof
instanceof
运算符希望左操作数是一个对象,右操作数标识对象的类。如果左侧的对象是右侧类的实例,则表达式返回 true
;否则返回 false
。后面会讲 JavaScript 中对象的类是通过初始化它们的构造函数来定义的。这样的话,instanceof
的右操作数应当是一个函数。比如:
var d = new Date(); // 通过 Date() 构造函数来创建一个新对象 d instanceof Date; // true,d 是由 Date() 创建的 d instanceof Object; // true,所有的对象都是 Object 的实例 d instanceof Number; // false,d 不是一个 Number 对象 var a = [1, 2, 3]; // 通过数组字面量的写法创建一个数组 a instanceof Array; // true,a 是一个数组 a instanceof Object; // true,所有的数组都是对象 a instanceof RegExp; // false,数组不是正则表达式
需要注意的是,所有的对象都是 Object
的实例。当通过 instanceof
判断一个对象是否是一个类的实例的时候,这个判断也会包含对「父类」的检测。如果 instanceof
的左操作数不是对象的话,instanceof
返回 false
。如果右操作数不是函数,则抛出一个类型错误异常。字符串
字符串
概述
字符串就是零个或多个排在一起的字符,放在单引号或双引号或反引号之中。
'abc' "abc"
单引号字符串的内部,可以使用双引号。双引号字符串的内部,可以使用单引号。
'key = "value"' "It's a long journey"
上面两个都是合法的字符串。
如果要在单引号字符串的内部,使用单引号,就必须在内部的单引号前面加上反斜杠,用来转义。双引号字符串内部使用双引号,也是如此。
'Did she say \'Hello\'?' // "Did she say 'Hello'?" "Did she say \"Hello\"?" // "Did she say "Hello"?"
由于 HTML 语言的属性值使用双引号,所以很多项目约定 JavaScript 语言的字符串只使用单引号。当然,只使用双引号也完全可以。重要的是坚持使用一种风格,不要一会使用单引号表示字符串,一会又使用双引号表示。
字符串默认只能写在一行内,分成多行将会报错。
'a b c' // SyntaxError: Unexpected token ILLEGAL
上面代码将一个字符串分成三行,JavaScript 就会报错。
如果长字符串必须分成多行,可以在每一行的尾部使用反斜杠。
var longString = 'Long \ long \ long \ string'; longString // "Long long long string"
上面代码表示,加了反斜杠以后,原来写在一行的字符串,可以分成多行书写。但是,输出的时候还是单行,效果与写在同一行完全一样。注意,反斜杠的后面必须是换行符,而不能有其他字符(比如空格),否则会报错。
连接运算符(+
)可以连接多个单行字符串,将长字符串拆成多行书写,输出的时候也是单行。
var longString = 'Long ' + 'long ' + 'long ' + 'string';
转义
反斜杠(\)在字符串内有特殊含义,用来表示一些特殊字符,所以又称为转义符。
需要用反斜杠转义的特殊字符,主要有下面这些。
-
\0
:null(\u0000
) -
\b
:后退键(\u0008
) -
\f
:换页符(\u000C
) -
\n
:换行符(\u000A
) -
\r
:回车键(\u000D
) -
\t
:制表符(\u0009
) -
\v
:垂直制表符(\u000B
) -
\'
:单引号(\u0027
) -
\"
:双引号(\u0022
) -
\\
:反斜杠(\u005C
)
上面这些字符前面加上反斜杠,都表示特殊含义。
console.log('1\n2') // 1 // 2
上面代码中,\n
表示换行,输出的时候就分成了两行。
反斜杠还有三种特殊用法。
(1)\HHH
反斜杠后面紧跟三个八进制数(000
到377
),代表一个字符。HHH
对应该字符的 Unicode 码点,比如\251
表示版权符号。显然,这种方法只能输出256种字符。
(2)\xHH
\x
后面紧跟两个十六进制数(00
到FF
),代表一个字符。HH
对应该字符的 Unicode 码点,比如\xA9
表示版权符号。这种方法也只能输出256种字符。
(3)\uXXXX
\u
后面紧跟四个十六进制数(0000
到FFFF
),代表一个字符。XXXX
对应该字符的 Unicode 码点,比如\u00A9
表示版权符号。
下面是这三种字符特殊写法的例子。
'\251' // "©" '\xA9' // "©" '\u00A9' // "©" '\172' === 'z' // true '\x7A' === 'z' // true '\u007A' === 'z' // true
如果在非特殊字符前面使用反斜杠,则反斜杠会被省略。
'\a' // "a"
上面代码中,a
是一个正常字符,前面加反斜杠没有特殊含义,反斜杠会被自动省略。
如果字符串的正常内容之中,需要包含反斜杠,则反斜杠前面需要再加一个反斜杠,用来对自身转义。
"Prev \\ Next" // "Prev \ Next"
字符串与数组
字符串可以被视为字符数组,因此可以使用数组的方括号运算符,用来返回某个位置的字符(位置编号从0开始)。
var s = 'hello'; s[0] // "h" s[1] // "e" s[4] // "o" // 直接对字符串使用方括号运算符 'hello'[1] // "e"
如果方括号中的数字超过字符串的长度,或者方括号中根本不是数字,则返回undefined
。
'abc'[3] // undefined 'abc'[-1] // undefined 'abc'['x'] // undefined
但是,字符串与数组的相似性仅此而已。实际上,无法改变字符串之中的单个字符。
var s = 'hello'; delete s[0]; s // "hello" s[1] = 'a'; s // "hello" s[5] = '!'; s // "hello"
上面代码表示,字符串内部的单个字符无法改变和增删,这些操作会默默地失败。
length 属性
length
属性返回字符串的长度,该属性也是无法改变的。
var s = 'hello'; s.length // 5 s.length = 3; s.length // 5 s.length = 7; s.length // 5
上面代码表示字符串的length
属性无法改变,但是不会报错。
字符串转换
1、String
函数可以将任意类型的值转化成字符串,转换规则如下。
(1)原始类型值
-
数值:转为相应的字符串。
-
字符串:转换后还是原来的值。
-
布尔值:
true
转为字符串"true"
,false
转为字符串"false"
。 -
undefined:转为字符串
"undefined"
。 -
null:转为字符串
"null"
。
String(123) // "123" String('abc') // "abc" String(true) // "true" String(undefined) // "undefined" String(null) // "null"
(2)对象
String
方法的参数如果是对象,返回一个类型字符串;如果是数组,返回该数组的字符串形式。
String({a: 1}) // "[object Object]" String([1, 2, 3]) // "1,2,3"
String
方法背后的转换规则,与Number
方法基本相同,只是互换了valueOf
方法和toString
方法的执行顺序。
-
先调用对象自身的
toString
方法。如果返回原始类型的值,则对该值使用String
函数,不再进行以下步骤。 -
如果
toString
方法返回的是对象,再调用原对象的valueOf
方法。如果valueOf
方法返回原始类型的值,则对该值使用String
函数,不再进行以下步骤。 -
如果
valueOf
方法返回的是对象,就报错。
下面是一个例子。
String({a: 1}) // "[object Object]" // 等同于 String({a: 1}.toString()) // "[object Object]"
上面代码先调用对象的toString
方法,发现返回的是字符串[object Object]
,就不再调用valueOf
方法了。
如果toString
法和valueOf
方法,返回的都是对象,就会报错。
var obj = { valueOf: function () { return {}; }, toString: function () { return {}; } }; String(obj) // TypeError: Cannot convert object to primitive value
下面是通过自定义toString
方法,改变返回值的例子。
String({ toString: function () { return 3; } }) // "3" String({ valueOf: function () { return 2; } }) // "[object Object]" String({ valueOf: function () { return 2; }, toString: function () { return 3; } }) // "3"
上面代码对三个对象使用String
函数。第一个对象返回toString
方法的值(数值3),第二个对象返回的还是toString
方法的值([object Object]
),第三个对象表示toString
方法先于valueOf
方法执行。
2、可以通过toString()方法来进行转换
toString()方法可见于数值,布尔值,对象和字符串值。
let a = 11; let aa = a.toString() console.log(aa)
多数情况下,toString()不接收任何参数。不过,在对数值调用这个方法时,toString()可以接收一个底数参数,即以什么底数来输出数值的字符串表示。默认情况下,toString()返回数值的十进制字符串表示
let num =10; console.log(num.toString(2)) console.log(num.toString(8)) console.log(num.toString(16))
模板字面量
模板字面量在定义模板时特别有用,比如下面这个html模板;
let pageHTML = ` <div > <a href="#"> <span>jake</span> </a> </div>`;
字符串插值
模板字面量最常用的一个特性是支持字符串插值,也就是可以在一个连续定义中插入一个或多个值。
字符串插值通过在${}中使用一个javascript表达式实现:
let value = 5; let exponent = 'second'; //以前,字符串插值是这样实现的 let val = value +' to the'+exponent; let val = `${value} to the ${exponent}`;
在插值表达式中可以调用函数和方法
function capitalize(word){ return `${word.toUpperCase()}`; } console.log(`${capitalize('hellow')}`)
布尔值
布尔值代表“真”和“假”两个状态。“真”用关键字true
表示,“假”用关键字false
表示。布尔值只有这两个值。
下列运算符会返回布尔值:
-
前置逻辑运算符:
!
(Not) -
相等运算符:
===
,!==
,==
,!=
-
比较运算符:
>
,>=
,<
,<=
如果 JavaScript 预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值。转换规则是除了下面六个值被转为false
,其他值都视为true
。
-
undefined
-
null
-
false
-
0
-
NaN
-
""
或''
(空字符串)
布尔值往往用于程序流程的控制,请看一个例子。
if ('') { console.log('true'); } // 没有任何输出
上面代码中,if
命令后面的判断条件,预期应该是一个布尔值,所以 JavaScript 自动将空字符串,转为布尔值false
,导致程序不会进入代码块,所以没有任何输出。
注意,空数组([]
)和空对象({}
)对应的布尔值,都是true
。
if ([]) { console.log('true'); } // true if ({}) { console.log('true'); } // true
Boolean()
Boolean
函数可以将任意类型的值转为布尔值。
它的转换规则相对简单:除了以下五个值的转换结果为false
,其他的值全部为true
。
-
undefined
-
null
-
-0
或+0
-
NaN
-
''
(空字符串)
Boolean(undefined) // false Boolean(null) // false Boolean(0) // false Boolean(NaN) // false Boolean('') // false
注意,所有对象(包括空对象)的转换结果都是true
,甚至连false
对应的布尔对象new Boolean(false)
也是true
(详见《原始类型值的包装对象》一章)。
Boolean({}) // true Boolean([]) // true Boolean(new Boolean(false)) // true
所有对象的布尔值都是true
,这是因为 JavaScript 语言设计的时候,出于性能的考虑,如果对象需要计算才能得到布尔值,对于obj1 && obj2
这样的场景,可能会需要较多的计算。为了保证性能,就统一规定,对象的布尔值为true
。
数值
整数和浮点数
JavaScript 内部,所有数字都是以64位浮点数形式储存,即使整数也是如此。所以,1
与1.0
是相同的,是同一个数。
1 === 1.0 // true
这就是说,JavaScript 语言的底层根本没有整数,所有数字都是小数。
由于Js采用的是IEEE 754浮点数运算标准,造成浮点数不是精确的值,所以涉及小数的比较和运算要特别小心。
0.1 + 0.2 === 0.3 // false 0.3 / 0.1 // 2.9999999999999996 (0.3 - 0.2) === (0.2 - 0.1) // false
数值范围
根据标准,64位浮点数的指数部分的长度是11个二进制位,意味着指数部分的最大值是2047(2的11次方减1)。也就是说,64位浮点数的指数部分的值最大为2047,分出一半表示负数,则 JavaScript 能够表示的数值范围为21024到2-1023(开区间),超出这个范围的数无法表示。
如果一个数大于等于2的1024次方,那么就会发生“正向溢出”,即 JavaScript 无法表示这么大的数,这时就会返回Infinity
。
Math.pow(2, 1024) // Infinity
如果一个数小于等于2的-1075次方(指数部分最小值-1023,再加上小数部分的52位),那么就会发生为“负向溢出”,即 JavaScript 无法表示这么小的数,这时会直接返回0。
Math.pow(2, -1075) // 0
下面是一个实际的例子。
var x = 0.5; for(var i = 0; i < 25; i++) { x = x * x; } x // 0
上面代码中,对0.5
连续做25次平方,由于最后结果太接近0,超出了可表示的范围,JavaScript 就直接将其转为0。
JavaScript 提供Number
对象的MAX_VALUE
和MIN_VALUE
属性,返回可以表示的具体的最大值和最小值。
Number.MAX_VALUE // 1.7976931348623157e+308 Number.MIN_VALUE // 5e-324
数值的表示法
JavaScript 的数值有多种表示方法,可以用字面形式直接表示,比如35
(十进制)和0xFF
(十六进制)。
数值也可以采用科学计数法表示,下面是几个科学计数法的例子。
123e3 // 123000 123e-3 // 0.123 -3.1E+12 .1e-23
科学计数法允许字母e
或E
的后面,跟着一个整数,表示这个数值的指数部分。
以下两种情况,JavaScript 会自动将数值转为科学计数法表示,其他情况都采用字面形式直接表示。
(1)小数点前的数字多于21位。
1234567890123456789012 // 1.2345678901234568e+21 123456789012345678901 // 123456789012345680000
(2)小数点后的零多于5个。
// 小数点后紧跟5个以上的零, // 就自动转为科学计数法 0.0000003 // 3e-7 // 否则,就保持原来的字面形式 0.000003 // 0.000003
数值的进制
使用字面量直接表示一个数值时,JavaScript 对整数提供四种进制的表示方法:十进制、十六进制、八进制、二进制。
-
十进制:没有前导0的数值。
-
八进制:有前缀
0o
或0O
的数值,或者有前导0、且只用到0-7的八个阿拉伯数字的数值。 -
十六进制:有前缀
0x
或0X
的数值,取值范围是0~9 a~f。 -
二进制:有前缀
0b
或0B
的数值。
默认情况下,JavaScript 内部会自动将八进制、十六进制、二进制转为十进制。下面是一些例子。
0xff // 255 0o377 // 255 0b11 // 3
如果八进制、十六进制、二进制的数值里面,出现不属于该进制的数字,就会报错。
0xzz // 报错 0o88 // 报错 0b22 // 报错
上面代码中,十六进制出现了字母z
、八进制出现数字8
、二进制出现数字2
,因此报错。
通常来说,有前导0的数值会被视为八进制,但是如果前导0后面有数字8
和9
,则该数值被视为十进制。
0888 // 888 0777 // 511
前导0表示八进制,处理时很容易造成混乱。ES5 的严格模式和 ES6,已经废除了这种表示法,但是浏览器为了兼容以前的代码,目前还继续支持这种表示法。
特殊数值
NaN
(1)含义
NaN
是 JavaScript 的特殊值,表示“非数字”(Not a Number),主要出现在将字符串解析成数字出错的场合。
5 - 'x' // NaN
上面代码运行时,会自动将字符串x
转为数值,但是由于x
不是数值,所以最后得到结果为NaN
,表示它是“非数字”(NaN
)。
另外,一些数学函数的运算结果会出现NaN
。
Math.log(-1) // NaN Math.sqrt(-1) // NaN
0
除以0
也会得到NaN
。
0 / 0 // NaN
需要注意的是,NaN
不是独立的数据类型,而是一个特殊数值,它的数据类型依然属于Number
,使用typeof
运算符可以看得很清楚。
typeof NaN // 'number'
(2)运算规则
NaN
不等于任何值,包括它本身。
NaN === NaN // false
数组的indexOf
方法内部使用的是严格相等运算符,所以该方法对NaN
不成立。
[NaN].indexOf(NaN) // -1
NaN
在布尔运算时被当作false
。
Boolean(NaN) // false
NaN
与任何数(包括它自己)的运算,得到的都是NaN
。
NaN + 32 // NaN NaN - 32 // NaN NaN * 32 // NaN NaN / 32 // NaN
Infinity
(1)含义
Infinity
表示“无穷”,用来表示两种场景。一种是一个正的数值太大,或一个负的数值太小,无法表示;另一种是非0数值除以0,得到Infinity
。
// 场景一 Math.pow(2, 1024) // Infinity // 场景二 0 / 0 // NaN 1 / 0 // Infinity
上面代码中,第一个场景是一个表达式的计算结果太大,超出了能够表示的范围,因此返回Infinity
。第二个场景是0
除以0
会得到NaN
,而非0数值除以0
,会返回Infinity
。
Infinity
有正负之分,Infinity
表示正的无穷,-Infinity
表示负的无穷。
Infinity === -Infinity // false 1 / -0 // -Infinity -1 / -0 // Infinity
上面代码中,非零正数除以-0
,会得到-Infinity
,负数除以-0
,会得到Infinity
。
由于数值正向溢出(overflow)、负向溢出(underflow)和被0
除,JavaScript 都不报错,所以单纯的数学运算几乎没有可能抛出错误。
Infinity
大于一切数值(除了NaN
),-Infinity
小于一切数值(除了NaN
)。
Infinity > 1000 // true -Infinity < -1000 // true
Infinity
与NaN
比较,总是返回false
。
Infinity > NaN // false -Infinity > NaN // false Infinity < NaN // false -Infinity < NaN // false
(2)运算规则
Infinity
的四则运算,符合无穷的数学计算规则。
5 * Infinity // Infinity 5 - Infinity // -Infinity Infinity / 5 // Infinity 5 / Infinity // 0
0乘以Infinity
,返回NaN
;0除以Infinity
,返回0
;Infinity
除以0,返回Infinity
。
0 * Infinity // NaN 0 / Infinity // 0 Infinity / 0 // Infinity
Infinity
加上或乘以Infinity
,返回的还是Infinity
。
Infinity + Infinity // Infinity Infinity * Infinity // Infinity
Infinity
减去或除以Infinity
,得到NaN
。
Infinity - Infinity // NaN Infinity / Infinity // NaN
Infinity
与null
计算时,null
会转成0,等同于与0
的计算。
null * Infinity // NaN null / Infinity // 0 Infinity / null // Infinity
Infinity
与undefined
计算,返回的都是NaN
。
undefined + Infinity // NaN undefined - Infinity // NaN undefined * Infinity // NaN undefined / Infinity // NaN Infinity / undefined // NaN
与数值相关的全局方法
parseInt()
(1)基本用法
parseInt
方法用于将字符串转为整数。
parseInt('123') // 123
如果字符串头部有空格,空格会被自动去除。
parseInt(' 81') // 81
如果parseInt
的参数不是字符串,则会先转为字符串再转换。
parseInt(1.23) // 1 // 等同于 parseInt('1.23') // 1
字符串转为整数的时候,是一个个字符依次转换,如果遇到不能转为数字的字符,就不再进行下去,返回已经转好的部分。
parseInt('8a') // 8 parseInt('12**') // 12 parseInt('12.34') // 12 parseInt('15e2') // 15 parseInt('15px') // 15
上面代码中,parseInt
的参数都是字符串,结果只返回字符串头部可以转为数字的部分。
如果字符串的第一个字符不能转化为数字(后面跟着数字的正负号除外),返回NaN
。
parseInt('abc') // NaN parseInt('.3') // NaN parseInt('') // NaN parseInt('+') // NaN parseInt('+1') // 1
所以,parseInt
的返回值只有两种可能,要么是一个十进制整数,要么是NaN
。
如果字符串以0x
或0X
开头,parseInt
会将其按照十六进制数解析。
parseInt('0x10') // 16
如果字符串以0
开头,将其按照10进制解析。
parseInt('011') // 11
对于那些会自动转为科学计数法的数字,parseInt
会将科学计数法的表示方法视为字符串,因此导致一些奇怪的结果。
parseInt(1000000000000000000000.5) // 1 // 等同于 parseInt('1e+21') // 1 parseInt(0.0000008) // 8 // 等同于 parseInt('8e-7') // 8
(2)进制转换
parseInt
方法还可以接受第二个参数(2到36之间),表示被解析的值的进制,返回该值对应的十进制数。默认情况下,parseInt
的第二个参数为10,即默认是十进制转十进制。
parseInt('1000') // 1000 // 等同于 parseInt('1000', 10) // 1000
下面是转换指定进制的数的例子。
parseInt('1000', 2) // 8 parseInt('1000', 6) // 216 parseInt('1000', 8) // 512
上面代码中,二进制、六进制、八进制的1000
,分别等于十进制的8、216和512。这意味着,可以用parseInt
方法进行进制的转换。
如果第二个参数不是数值,会被自动转为一个整数。这个整数只有在2到36之间,才能得到有意义的结果,超出这个范围,则返回NaN
。如果第二个参数是0
、undefined
和null
,则直接忽略。
parseInt('10', 37) // NaN parseInt('10', 1) // NaN parseInt('10', 0) // 10 parseInt('10', null) // 10 parseInt('10', undefined) // 10
如果字符串包含对于指定进制无意义的字符,则从最高位开始,只返回可以转换的数值。如果最高位无法转换,则直接返回NaN
。
parseInt('1546', 2) // 1 parseInt('546', 2) // NaN
上面代码中,对于二进制来说,1
是有意义的字符,5
、4
、6
都是无意义的字符,所以第一行返回1,第二行返回NaN
。
前面说过,如果parseInt
的第一个参数不是字符串,会被先转为字符串。这会导致一些令人意外的结果。
parseInt(0x11, 36) // 43 parseInt(0x11, 2) // 1 // 等同于 parseInt(String(0x11), 36) parseInt(String(0x11), 2) // 等同于 parseInt('17', 36) parseInt('17', 2)
上面代码中,十六进制的0x11
会被先转为十进制的17,再转为字符串。然后,再用36进制或二进制解读字符串17
,最后返回结果43
和1
。
这种处理方式,对于八进制的前缀0,尤其需要注意。
parseInt(011, 2) // NaN // 等同于 parseInt(String(011), 2) // 等同于 parseInt(String(9), 2)
上面代码中,第一行的011
会被先转为字符串9
,因为9
不是二进制的有效字符,所以返回NaN
。如果直接计算parseInt('011', 2)
,011
则是会被当作二进制处理,返回3。
JavaScript 不再允许将带有前缀0的数字视为八进制数,而是要求忽略这个0
。但是,为了保证兼容性,大部分浏览器并没有部署这一条规定。
parseFloat()
parseFloat
方法用于将一个字符串转为浮点数。
parseFloat('3.14') // 3.14
如果字符串符合科学计数法,则会进行相应的转换。
parseFloat('314e-2') // 3.14 parseFloat('0.0314E+2') // 3.14
如果字符串包含不能转为浮点数的字符,则不再进行往后转换,返回已经转好的部分。
parseFloat('3.14more non-digit characters') // 3.14
parseFloat
方法会自动过滤字符串前导的空格。
parseFloat('\t\v\r12.34\n ') // 12.34
如果参数不是字符串,或者字符串的第一个字符不能转化为浮点数,则返回NaN
。
parseFloat([]) // NaN parseFloat('FF2') // NaN parseFloat('') // NaN
上面代码中,尤其值得注意,parseFloat
会将空字符串转为NaN
。
这些特点使得parseFloat
的转换结果不同于Number
函数。
parseFloat(true) // NaN Number(true) // 1 parseFloat(null) // NaN Number(null) // 0 parseFloat('') // NaN Number('') // 0 parseFloat('123.45#') // 123.45 Number('123.45#') // NaN
Number()
使用Number
函数,可以将任意类型的值转化成数值。
下面分成两种情况讨论,一种是参数是原始类型的值,另一种是参数是对象。
(1)原始类型值
原始类型值的转换规则如下。
// 数值:转换后还是原来的值 Number(324) // 324 // 字符串:如果可以被解析为数值,则转换为相应的数值 Number('324') // 324 // 字符串:如果不可以被解析为数值,返回 NaN Number('324abc') // NaN // 空字符串转为0 Number('') // 0 // 布尔值:true 转成 1,false 转成 0 Number(true) // 1 Number(false) // 0 // undefined:转成 NaN Number(undefined) // NaN // null:转成0 Number(null) // 0
Number
函数将字符串转为数值,要比parseInt
函数严格很多。基本上,只要有一个字符无法转成数值,整个字符串就会被转为NaN
。
parseInt('42 cats') // 42 Number('42 cats') // NaN
上面代码中,parseInt
逐个解析字符,而Number
函数整体转换字符串的类型。
另外,parseInt
和Number
函数都会自动过滤一个字符串前导和后缀的空格。
parseInt('\t\v\r12.34\n') // 12 Number('\t\v\r12.34\n') // 12.34
(2)对象
简单的规则是,Number
方法的参数是对象时,将返回NaN
,除非是包含单个数值的数组。
Number({a: 1}) // NaN Number([1, 2, 3]) // NaN Number([5]) // 5
之所以会这样,是因为Number
背后的转换规则比较复杂。
第一步,调用对象自身的valueOf
方法。如果返回原始类型的值,则直接对该值使用Number
函数,不再进行后续步骤。
第二步,如果valueOf
方法返回的还是对象,则改为调用对象自身的toString
方法。如果toString
方法返回原始类型的值,则对该值使用Number
函数,不再进行后续步骤。
第三步,如果toString
方法返回的是对象,就报错。
请看下面的例子。
var obj = {x: 1}; Number(obj) // NaN // 等同于 if (typeof obj.valueOf() === 'object') { Number(obj.toString()); } else { Number(obj.valueOf()); }
上面代码中,Number
函数将obj
对象转为数值。背后发生了一连串的操作,首先调用obj.valueOf
方法, 结果返回对象本身;于是,继续调用obj.toString
方法,这时返回字符串[object Object]
,对这个字符串使用Number
函数,得到NaN
。
默认情况下,对象的valueOf
方法返回对象本身,所以一般总是会调用toString
方法,而toString
方法返回对象的类型字符串(比如[object Object]
)。所以,会有下面的结果。
Number({}) // NaN
如果toString
方法返回的不是原始类型的值,结果就会报错。
var obj = { valueOf: function () { return {}; }, toString: function () { return {}; } }; Number(obj) // TypeError: Cannot convert object to primitive value
上面代码的valueOf
和toString
方法,返回的都是对象,所以转成数值时会报错。
从上例还可以看到,valueOf
和toString
方法,都是可以自定义的。
Number({ valueOf: function () { return 2; } }) // 2 Number({ toString: function () { return 3; } }) // 3 Number({ valueOf: function () { return 2; }, toString: function () { return 3; } }) // 2
上面代码对三个对象使用Number
函数。第一个对象返回valueOf
方法的值,第二个对象返回toString
方法的值,第三个对象表示valueOf
方法先于toString
方法执行。
isNaN()
isNaN
方法可以用来判断一个值是否为NaN
。
isNaN(NaN) // true isNaN(123) // false
但是,isNaN
只对数值有效,如果传入其他值,会被先转成数值。比如,传入字符串的时候,字符串会被先转成NaN
,所以最后返回true
,这一点要特别引起注意。也就是说,isNaN
为true
的值,有可能不是NaN
,而是一个字符串。
isNaN('Hello') // true // 相当于 isNaN(Number('Hello')) // true
出于同样的原因,对于对象和数组,isNaN
也返回true
。
isNaN({}) // true // 等同于 isNaN(Number({})) // true isNaN(['xzy']) // true // 等同于 isNaN(Number(['xzy'])) // true
但是,对于空数组和只有一个数值成员的数组,isNaN
返回false
。
isNaN([]) // false isNaN([123]) // false isNaN(['123']) // false
上面代码之所以返回false
,原因是这些数组能被Number
函数转成数值,请参见《数据类型转换》一章。
因此,使用isNaN
之前,最好判断一下数据类型。
function myIsNaN(value) { return typeof value === 'number' && isNaN(value); }
判断NaN
更可靠的方法是,利用NaN
为唯一不等于自身的值的这个特点,进行判断。
function myIsNaN(value) { return value !== value; }
null, undefined
概述
null
与undefined
都可以表示“没有”,含义非常相似。将一个变量赋值为undefined
或null
。
var a = undefined; 0.3------------------- // 或者 var a = null;
上面代码中,变量a
分别被赋值为undefined
和null
,这两种写法的效果几乎等价。
在if
语句中,它们都会被自动转为false
,相等运算符(==
)甚至直接报告两者相等。
if (!undefined) { console.log('undefined is false'); } // undefined is false if (!null) { console.log('null is false'); } // null is false undefined == null // true
从上面代码可见,两者的行为是何等相似!谷歌公司开发的 JavaScript 语言的替代品 Dart 语言,就明确规定只有null
,没有undefined
!
既然含义与用法都差不多,为什么要同时设置两个这样的值,这不是无端增加复杂度,令初学者困扰吗?这与历史原因有关。
1995年 JavaScript 诞生时,最初像 Java 一样,只设置了null
表示"无"。根据 C 语言的传统,null
可以自动转为0
。
Number(null) // 0 5 + null // 5
上面代码中,null
转为数字时,自动变成0。
但是,JavaScript 的设计者 Brendan Eich,觉得这样做还不够。首先,第一版的 JavaScript 里面,null
就像在 Java 里一样,被当成一个对象,Brendan Eich 觉得表示“无”的值最好不是对象。其次,那时的 JavaScript 不包括错误处理机制,Brendan Eich 觉得,如果null
自动转为0,很不容易发现错误。
因此,他又设计了一个undefined
。区别是这样的:null
是一个表示“空”的对象,转为数值时为0
;undefined
是一个表示"此处无定义"的原始值,转为数值时为NaN
。
Number(undefined) // NaN 5 + undefined // NaN
用法和含义
对于null
和undefined
,大致可以像下面这样理解。
null
表示空值,即该处的值现在为空。调用函数时,某个参数未设置任何值,这时就可以传入null
,表示该参数为空。比如,某个函数接受引擎抛出的错误作为参数,如果运行过程中未出错,那么这个参数就会传入null
,表示未发生错误。
undefined
表示“未定义”,下面是返回undefined
的典型场景。
// 变量声明了,但没有赋值 var i; i // undefined // 调用函数时,应该提供的参数没有提供,该参数等于 undefined function f(x) { return x; } f() // undefined // 对象没有赋值的属性 var o = new Object(); o.p // undefined // 函数没有返回值时,默认返回 undefined function f() {} f() // undefined
数据类型的转换
概述
JavaScript 是一种动态类型语言,变量没有类型限制,可以随时赋予任意值。
var x = y ? 1 : 'a';
上面代码中,变量x
到底是数值还是字符串,取决于另一个变量y
的值。y
为true
时,x
是一个数值;y
为false
时,x
是一个字符串。这意味着,x
的类型没法在编译阶段就知道,必须等到运行时才能知道。
虽然变量的数据类型是不确定的,但是各种运算符对数据类型是有要求的。如果运算符发现,运算的类型与预期不符,就会自动转换类型。比如,减法运算符预期左右两侧的运算子应该是数值,如果不是,就会自动将它们转为数值。
'4' - '3' // 1
上面代码中,虽然是两个字符串相减,但是依然会得到结果数值1
,原因就在于 JavaScript 将运算子自动转为了数值。
本章讲解数据类型自动转换的规则。在此之前,先讲解如何手动强制转换数据类型。
自动转换
下面介绍自动转换,它是以强制转换为基础的。
遇到以下三种情况时,JavaScript 会自动转换数据类型,即转换是自动完成的,用户不可见。
第一种情况,不同类型的数据互相运算。
123 + 'abc' // "123abc"
第二种情况,对非布尔值类型的数据求布尔值。
if ('abc') { console.log('hello') } // "hello"
第三种情况,对非数值类型的值使用一元运算符(即+
和-
)。
+ {foo: 'bar'} // NaN - [1, 2, 3] // NaN
自动转换的规则是这样的:预期什么类型的值,就调用该类型的转换函数。比如,某个位置预期为字符串,就调用String
函数进行转换。如果该位置即可以是字符串,也可能是数值,那么默认转为数值。
由于自动转换具有不确定性,而且不易除错,建议在预期为布尔值、数值、字符串的地方,全部使用Boolean
、Number
和String
函数进行显式转换。
自动转换为布尔值
JavaScript 遇到预期为布尔值的地方(比如if
语句的条件部分),就会将非布尔值的参数自动转换为布尔值。系统内部会自动调用Boolean
函数。
因此除了以下五个值,其他都是自动转为true
。
-
undefined
-
null
-
+0
或-0
-
NaN
-
''
(空字符串)
下面这个例子中,条件部分的每个值都相当于false
,使用否定运算符后,就变成了true
。
if ( !undefined && !null && !0 && !NaN && !'' ) { console.log('true'); } // true
下面两种写法,有时也用于将一个表达式转为布尔值。它们内部调用的也是Boolean
函数。
// 写法一 expression ? true : false // 写法二 !! expression
自动转换为字符串
JavaScript 遇到预期为字符串的地方,就会将非字符串的值自动转为字符串。具体规则是,先将复合类型的值转为原始类型的值,再将原始类型的值转为字符串。
字符串的自动转换,主要发生在字符串的加法运算时。当一个值为字符串,另一个值为非字符串,则后者转为字符串。
'5' + 1 // '51' '5' + true // "5true" '5' + false // "5false" '5' + {} // "5[object Object]" '5' + [] // "5" '5' + function (){} // "5function (){}" '5' + undefined // "5undefined" '5' + null // "5null"
这种自动转换很容易出错。
var obj = { width: '100' }; obj.width + 20 // "10020"
上面代码中,开发者可能期望返回120
,但是由于自动转换,实际上返回了一个字符10020
。
自动转换为数值
JavaScript 遇到预期为数值的地方,就会将参数值自动转换为数值。系统内部会自动调用Number
函数。
除了加法运算符(+
)有可能把运算子转为字符串,其他运算符都会把运算子自动转成数值。
'5' - '2' // 3 '5' * '2' // 10 true - 1 // 0 false - 1 // -1 '1' - 1 // 0 '5' * [] // 0 false / '5' // 0 'abc' - 1 // NaN null + 1 // 1 undefined + 1 // NaN
上面代码中,运算符两侧的运算子,都被转成了数值。
注意:
null
转为数值时为0
,而undefined
转为数值时为NaN
。
一元运算符也会把运算子转成数值。
+'abc' // NaN -'abc' // NaN +true // 1 -false // 0