什么是 JavaScript,它有什么特点?对于客户端原生开发者来说,在学习 JavaScript 过程中,有哪些不得不注意的点?
背景
JavaScript 是一种轻量级的脚本语言,同时也是一门动态类型语言。对于 Android 或 iOS
开发者来说,JavaScript 的动态类型特性会让我们在学习过程中遇到各种“坑”,下面将学习过程中需要注意的点罗列出来作为后续备忘使用。(JavaScript 基于 ECMAScript 5.1 版本,更多历史背景可参考 JavaScript 语言的历史)
动态语言和静态语言
动态语言
是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。通俗点说就是在运行时代码可以根据某些条件改变自身结构。
主要动态语言:Object-C、C#、JavaScript、PHP、Python、Erlang。
静态语言
与动态语言相对应的,运行时结构不可变的语言就是静态语言。如Java、C、C++。
注意
很多人认为解释型语言都是动态语言,这个观点是错的!Java 是解释型语言但是不是动态语言,Java 不能在运行的时候改变自己结构。反之成立吗?动态语言都是解释型语言。也是错的!Object-C 是编译型语言,但是他是动态语言。得益于特有的 run time 机制(准确说 run time 不是语法特性是运行时环境,这里不展开)OC 代码是可以在运行的时候插入、替换方法的。
动态类型语言和静态类型语言
动态类型语言
动态类型语言和动态语言是完全不同的两个概念。动态类型语言是指在运行期间才去做数据类型检查的语言,说的是数据类型
,动态语言说的是运行是改变结构,说的是代码结构
。
动态类型语言的数据类型不是在编译阶段决定的,而是把类型绑定延后到了运行阶段。
主要语言:Python、Ruby、Erlang、JavaScript、swift、PHP、Perl。
静态类型语言
静态语言的数据类型是在编译其间确定的或者说运行之前确定的,编写代码的时候要明确确定变量的数据类型。
主要语言:C、C++、C#、Java、Object-C。
动态类型语言和静态类型语言优缺点
语言 | 优点 | 缺点 |
---|---|---|
静态类型 | 结构规范,便于调试,方便类型安全 | 额外编写类型相关代码,代码量增加,注意判断变量类型 |
动态类型 | 提高了编码灵活性,代码量少,方便阅读 | 不便调试,易发生类型相关错误 |
以上我们基本了解了动态语言和静态语言、动态类型语言和静态类型语言以及它们的区别。对于 Java、Object-C 静态类型语言开发者,初学 JavaScript 动态类型语言,可能会有一些点需要特别注意,以下我们将常见易混淆的点进行相关罗列说明。
数据类型
- 数值(number):整数和小数(比如1和3.14)。
- 字符串(string):文本(比如 Hello World )。
- 布尔值(boolean):表示真伪的两个特殊值,即 true(真)和 false(假)。
- undefined:表示“未定义”或不存在,即由于目前没有定义,所以此处暂时没有任何值。
- null:表示空值,即此处的值为空。
- 对象(object):各种值组成的集合。
- Symbol :表示独一无二的值,ES6 引入,此文不考虑。
通常,数值、字符串、布尔值这三种类型,合称为原始类型(primitive type);对象是合成类型(complex type)。
变量
变量提升
JavaScript 引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升(hoisting)。
1 | console.log(a); |
上面代码首先使用 console.log 方法,在控制台(console)显示变量 a 的值。这时变量 a 还没有声明和赋值,所以这是一种错误的做法,但是实际上不会报错。因为存在变量提升,真正运行的是下面的代码。
1 | var a; |
null 和 undefied
undefined
和 null
一般认为是两个特殊值。if
语句中,它们均被认为是 false
; 相等运算符 ==
两者是相等的。
1 | if (!undefined) { |
在转为数值时,null
会转为 0,undefined
会转为 NaN
。
布尔值
如果 JavaScript 预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值。转换规则是除了下面六个值被转为 false,其他值都视为 true。
undefined
null
false
0
NaN
""
或''
(空字符串)
更多详情可参考:null, undefined 和布尔值
数值
整数和浮点数
JavaScript 内部,所有数字都是以 64 位浮点数形式储存,即使整数也是如此。由于浮点数不是精确值,涉及小数的比较和运算需要特别小心。
1 | 1 === 1.0 // true |
正零和负零
JavaScript 内部实际上存在 2 个 0:一个是 +0,一个是 -0,区别就是 64 位浮点数表示法的符号位不同。它们是等价的。
几乎所有场合,正零和负零都会被当作正常的 0。唯一区别的场合,是 +0
或 -0
当作分母,返回值不一样,一个是 +Infinity
,另一个是 -Infinity
。
1 | -0 === +0 // true |
NaN
NaN
是 JavaScript 的特殊值,表示“非数字”(Not a Number),主要出现在将字符串解析成数字出错的场合,一些数学函数运算结果也会出现 NaN
。
1 | 5 - 'x' // NaN |
与数值相关的方法
parseInt
方法用于将字符串转为整数。
1 | parseInt('123') // 123 |
parseFloat
方法用于将一个字符串转为浮点数。
1 | parseFloat('3.14') // 3.14 |
isNaN()
方法可以用来判断一个值是否为NaN。
1 | isNaN(NaN) // true |
isFinite
方法返回一个布尔值,表示某个值是否为正常的数值。
1 | // 除了Infinity、-Infinity、NaN 和 undefined 这几个值会返回 false,isFinite 对于其他的数值都会返回 true。 |
更多详情请参考:数值
对象
对象(object)是 JavaScript 语言的核心概念,也是最重要的数据类型。
什么是对象?简单说,对象就是一组“键值对”(key-value)的集合,是一种无序的复合数据集合。
属性的删除
delete
命令用于删除对象的属性,删除成功后返回 true
。
1 | var obj = { p: 1 }; |
属性是否存在:in 运算符
in
运算符用于检查对象是否包含某个属性(注意,检查的是键名,不是键值),如果包含就返回 true,否则返回 false。
in
运算符的一个问题是,它不能识别哪些属性是对象自身的,哪些属性是继承的。
属性的遍历
for...in
循环用来遍历一个对象的全部属性。
- 它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性
- 它不仅遍历对象自身的属性,还遍历继承的属性。
with 语句
with
语句格式如下
1 | with (对象) { |
它的作用是操作同一个对象的多个属性时,提供一些书写的方便。注意,如果 with 区块内部有变量的赋值操作,必须是当前对象已经存在的属性,否则会创造一个当前作用域的全局变量。
1 | var obj = {}; |
这是因为 with 区块没有改变作用域,它的内部依然是当前作用域。这造成了with语句的一个很大的弊病,就是绑定对象不明确。这非常不利于代码的除错和模块化,编译器也无法对这段代码进行优化,只能留到运行时判断,这就拖慢了运行速度。因此,建议不要使用 with 语句,可以考虑用一个临时变量代替 with。
更多详情可参考:对象
函数
JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。函数只是一个可以执行的值,此外并无特殊之处。
由于函数与其他数据类型地位平等,所以在 JavaScript 语言中又称函数为第一等公民。
函数名的提升
JavaScript 引擎将函数名视同变量名,所以采用 function 命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。所以,下面的代码不会报错。但是,如果采用赋值语句定义函数,由于“变量提升”,JavaScript 就会报错。
1 | f(); |
注意,如果像下面例子那样,采用 function 命令和 var 赋值语句声明同一个函数,由于存在函数提升,最后会采用 var 赋值语句的定义。
1 | var f = function () { |
length 属性
函数的 length 属性返回函数预期传入的参数个数,即函数定义之中的参数个数。不管调用时输入了多少个参数,length 属性始终等于定义时参数个数。
1 | function f(a, b) {} |
立即调用的函数表达式(IIFE)
根据 JavaScript 的语法,圆括号 () 跟在函数名之后,表示调用该函数。参考:立即调用的函数表达式
更多详情请参考:函数
数组
数组(array)是按次序排列的一组值。每个值的位置都有编号(从 0 开始),整个数组用方括号表示。本质上,数组属于一种特殊的对象。typeof 运算符会返回数组的类型是 object。
数组 length 属性
数组的 length 属性,返回数组的成员数量。length 属性的值总是比最大的那个整数键大 1。另外,这也表明数组是一种动态的数据结构,可以随时增减数组的成员。
1 | var arr = [ 'a', 'b', 'c' ]; |
in 运算符
检查某个键名是否存在的运算符in,适用于对象,也适用于数组。
1 | // 由于键名都是字符串,数值会转成字符串 |
数组的空位
当数组的某个位置是空元素,即两个逗号之间没有任何值,我们称该数组存在空位(hole)。
1 | var a = [1, , 1]; |
数组的某个位置是空位,与某个位置是 undefined,是不一样的。如果是空位,使用数组的 forEach 方法、for…in 结构、以及 Object.keys 方法进行遍历,空位都会被跳过。如果某个位置是 undefined,遍历的时候就不会被跳过。
类似数组的对象
如果一个对象的所有键名都是正整数或零,并且有 length 属性,那么这个对象就很像数组,语法上称为“类似数组的对象”(array-like object)。但是,“类似数组的对象”并不是数组,因为它们不具备数组特有的方法。对象没有数组的 push 方法,使用该方法就会报错。
“类似数组的对象”的根本特征,就是具有 length 属性。只要有 length 属性,就可以认为这个对象类似于数组。但是有一个问题,这种length 属性不是动态值,不会随着成员的变化而变化。
1 | var obj = { |
算术运算符
加法运算符
加法运算符(+
)是最常见的运算符,用来求两个数值的和。
1 | 1 + 1 // 2 |
余数运算符
余数运算符(%
)返回前一个运算子被后一个运算子除,所得的余数。
1 | 12 % 5 // 2 |
比较运算符
1 | // 字符串按照字典顺序进行比较 |
严格相等运算符
JavaScript 提供两种相等运算符:==
和 ===
。
简单说,它们的区别是相等运算符(==
)比较两个值是否相等,严格相等运算符(===
)比较它们是否为“同一个值”。如果两个值不是同一类型,严格相等运算符(===
)直接返回 false,而相等运算符(==
)会将它们转换成同一个类型,再用严格相等运算符进行比较。
1 | // 如果两个值的类型不同,直接返回false。 |
相等运算符
相等运算符用来比较相同类型的数据时,与严格相等运算符完全一样。
比较不同类型的数据时,相等运算符会先将数据进行类型转换,然后再用严格相等运算符比较。详情可参考:相等运算符
布尔运算符
且运算符(&&
) 往往用于多个表达式的求值。
它的运算规则是:如果第一个运算子的布尔值为 true,则返回第二个运算子的值(注意是值,不是布尔值);如果第一个运算子的布尔值为 false,则直接返回第一个运算子的值,且不再对第二个运算子求值。这种跳过第二个运算子的机制,被称为短路
。
且运算符可以多个连用,这时返回第一个布尔值为 false 的表达式的值。如果所有表达式的布尔值都为 true,则返回最后一个表达式的值。
1 | 't' && '' // "" |
或运算符(||
)也用于多个表达式的求值。它的运算规则是:如果第一个运算子的布尔值为 true,则返回第一个运算子的值,且不再对第二个运算子求值;如果第一个运算子的布尔值为 false,则返回第二个运算子的值。
或运算符可以多个连用,这时返回第一个布尔值为 true 的表达式的值。如果所有表达式都为 false,则返回最后一个表达式的值。
1 | 't' || '' // "t" |
更多运算符详情请参考:运算符
数据类型转换
强制转换主要指使用 Number()、String() 和 Boolean() 三个函数,手动将各种类型的值,分别转换成数字、字符串或者布尔值。
Number ()
Number 函数将字符串转为数值,要比 parseInt 函数严格很多。基本上,只要有一个字符无法转成数值,整个字符串就会被转为 NaN。
1 | // 数值:转换后还是原来的值 |
Number 背后的转换规则比较复杂。
第一步,调用对象自身的 valueOf 方法。如果返回原始类型的值,则直接对该值使用 Number 函数,不再进行后续步骤。
第二步,如果 valueOf 方法返回的还是对象,则改为调用对象自身的 toString 方法。如果 toString 方法返回原始类型的值,则对该值使用 Number 函数,不再进行后续步骤。
第三步,如果 toString 方法返回的是对象,就报错。
Boolean()
Boolean() 函数可以将任意类型的值转为布尔值。除了以下五个值的转换结果为 false,其他的值全部为 true。
undefined
null
0
(包含-0
和+0
)NaN
''
(空字符串)
1 | Boolean(undefined) // false |
更多详情请参考:数据类型转换
语法
分号表示一条语句的结束。JavaScript 允许省略行尾的分号。以下三种情况,语法规定不需要在结尾添加分号,如果添加也不会出错,因为解释引擎会把这个分号解释为空语句。
- for 和 while 循环
- 分支语句:if,switch,try
- 函数声明语句
除了上面三种情况,所有语句都应该使用分号。同时,对于大多数情况,JavaScrip 会自动添加分号。这种语法特性被称为 “分号的自动添加”(Automatic Semicolon Insertion,简称 ASI
)。由于解释引擎自动添加分号的行为难以预测,因此编写代码的时候不应该省略行尾的分号。
详情参考:行尾的分号
标准库对象及方法
Array 对象
数组构造函数的行为不统一:Array 构造函数
Array 的一些操作符,哪些改变原数组,哪些不改变原数组 array
方法 | 作用 | 是否改变原数组 |
---|---|---|
valueOf() | 对该对象求值,不同对象的实现不一致,数组的valueOf方法返回数组本身 | 否 |
toString() | 是对象的通用方法,数组的toString方法返回数组的字符串形式 | 否 |
push() | 用于在数组的末端添加一个或多个元素,并返回添加新元素后的数组长度 | 是 |
pop() | 用于删除数组的最后一个元素,并返回该元素 | 是 |
shift() | 用于删除数组的第一个元素,并返回该元素 | 是 |
unshift() | 用于在数组的第一个位置添加元素,并返回添加新元素后的数组长度 | 是 |
join() | 以指定参数作为分隔符,将所有数组成员连接为一个字符串返回。如果不提供参数,默认用逗号分隔 | 否 |
concat() | 用于多个数组的合并。它将新数组的成员,添加到原数组成员的后部,然后返回一个新数组 | 否 |
reverse() | 用于颠倒排列数组元素,返回改变后的数组 | 是 |
slice() | 用于提取目标数组的一部分,返回一个新数组 | 否 |
splice() | 用于删除原数组的一部分成员,并可以在删除的位置添加新的数组成员,返回值是被删除的元素 | 是 |
sort() | 对数组成员进行排序,默认是按照字典顺序排序 | 是 |
map() | 将数组的所有成员依次传入参数函数,然后把每一次的执行结果组成一个新数组返回 | 否 |
forEach() | 对数组的所有成员依次执行参数函数,但是不返回值,只用来操作数据 | 否 |
filter() | 用于过滤数组成员,满足条件的成员组成一个新数组返回 | 否 |
some() | 类似“断言”(assert),返回一个布尔值,表示判断数组成员是否符合某种条件。只要一个成员的返回值是true,则整个some方法的返回值就是true,否则返回false | 否 |
every() | 类似“断言”(assert),返回一个布尔值,表示判断数组成员是否符合某种条件; 所有成员的返回值都是true,整个every方法才返回true,否则返回false | 否 |
reduce() | 依次处理数组的每个成员,最终累计为一个值。从左到右处理(从第一个成员到最后一个成员) | 否 |
reduceRight() | 依次处理数组的每个成员,最终累计为一个值。从右到左(从最后一个成员到第一个成员) | 否 |
indexOf() | 返回给定元素在数组中第一次出现的位置,如果没有出现则返回-1 | 否 |
lastIndexOf() | 返回给定元素在数组中最后一次出现的位置,如果没有出现则返回-1 | 否 |
Number 对象
Number 对象的实例方法,数值需要放在括号里,不然会被 JavaScript 引擎解释成小数点,从而报错。
只要能够让 JavaScript 引擎不混淆小数点和对象的点运算符,各种写法都能用。除了为数值加上括号,还可以在数值后面加两个点,JavaScript 会把第一个点理解成小数点,把第二个点理解成调用对象属性,从而得到正确结果
toFixed()
toFixed() 方法先将一个数转为指定位数的小数,然后返回这个小数对应的字符串。由于浮点数的原因,小数 5 的四舍五入是不确定的,使用的时候必须小心。
String 对象
String 实例方法,substring 违反直觉,建议使用 slice 方法。substring
总结
本文简单说明了动态类型语言、静态类型语言的区别,归纳总结了 JavaScript 学习过程中容易混淆以及出错的点,尤其是数据类型和运算符这块。对于初学 JavaScript 语言者,希望可以起到一定的帮助。