《JavaScript 高级程序设计》第三章:基本概念

目录

  • 语法
  • 标识符
  • 严格模式
  • 关键字
  • 保留字
  • 变量
  • 数据类型
  • 运算符
  • 表达式与语句

语法

“语法”指的是一门语言的书写风格,JavaScript 的语法风格很类似于 C 以及 Java。
语法又是一种概念上的统称,例如如何声明变量、定义函数、书写流程控制语句或者是进行条件判断等,这些都是属于语法风格的范畴。

标识符

所谓的“标识符”就是对变量、标签、函数、方法、属性以及参数名称的统称。
标识符可以分为“用户自定义标识符”与“系统标识符”,其中对与系统标识符,我们可以根据其使用状况,又划分为“关键字”或者是“保留字”。
用户自定义标识符,其命名应遵循以下规则:

  • 开头只能以字母,下划线(_)、美元符号($)、π等字符开头。
  • 除了开头的字符外,标识符的其它字符可以是字母、数字、下划线、美元符号以及其它ASCII码字符或者是扩展ASCII码字符(当然Unicode中的字符也是可以的,但并不建议这么做)
  • 在命名的格式上,我们强烈建议驼峰命名法。

严格模式

通过声明 use strict 可以让支持严格模式的 JavaScript引擎切换到另一种解析与执行模式,在严格模式下 ES3中的一些不确定行为将会按照ES5(ES3.1)的规定去处理。
use strict 如果声明在代码的第一行,则整个JS程序都会按照严格模式去执行,如果你只想在某个局部区域使用该模式,请将 use strict 单独加在某个局部作用域的开头。

关键字

用于特定用途的标识符,这些标识符可用于表示控制流程的开始或结束,或者用于执行特定的操作,例如声明变量、定义函数等。

保留字

保留字是语言层面保留的标识符,它与关键字的主要区别就在于作为保留字的标识符暂时还未指定用途,但它们未来很可能会用作于关键字。

变量

ECMAScript中的变量是弱类型的,相当于保存值的占位符。
声明但未初始化的变量,其默认值为 undefined
ECMAScript支持同时声明并初始化多个变量,每个变量用逗号隔开

var obj = {},
    str = 'string',
    num = 0,
    nul = null,
    und = undefined,
    boolean = true;

在ECMAScript中不通过 var 关键字声明的变量,默认会附加到全局 window 对象上。

a = 1;
window.a // 1

该种方式在 ECMAScript 5 的严格模式(strict)下是不被允许的。

数据类型

下面是ECMAScript5 的数据类型:

ecmascript-type.png-12.5kB

对于数据类型的检测,JavaScript中也提供了一个内置的操作符 -- typeof,通过 typeof 我们可以准确的获得"string"、"number"、"boolean"、"undefined"等基本数据类型,但是需要注意的是使用 typeofnull 或者其它对象型或者是数组检测的时候,统一都返回 object 类型。

Undefined

undefined 类型只有一个值,那就是:"undefined"。
一般而言我们声明未初始化的变量其默认值就是 undefined,而ECMAScript引入 undefined 的作用就是为了更好的与 null 进行区分。
这里有一个技巧,使用 typeof 操作符来判断未声明的变量,并不会如我们想的那样会报错,而是返回 undefined 值。

typeof und // undefined 

Null

nullundefined 相同都是只有一个值的数据类型。
null 代表一个 “空对象指针”,这也是为什么使用 typeof null 返回的却是一个 object类型的原因。
null 对于哪些需要保存为对象但还未真正保存具体对象引用的变量非常有用。也就是说如果定义的变量专门是用来保存对象,那么最好在定义变量的同时初始化为 null 而不是其它值,这样带来的好处体现于,只要检测 null 值就可以得知变量是否保存了一个真正对象的引用。

var obj = null;

if(typeof obj === "object" && obj != null){
    //....
}

由于 nundefined的值是派生与 null(其实很多的数据类型都是派生与null,因为对象原型链的源头就是null),因此ECMAScript的语法规定在做相等性检测的时候返回固定值true

null == undefined // true

总的来说,null是专门用来初始化需要保存对象引用的变量,这也是 nullundefined之间的重要区别。当变量用于保存基本数据类型时,无需要初始化为 undefined,因为默认就是 undefined,而对于用于保存对象的变量 null 则需要显示的初始化,这既是用于区分一个变量是否引用了一个有效的对象,也有助于进一步区分 undefinednull

var age;
var name;
var sex;
var othre = null;

age = 26;
name = 'gtshen';
sex = 'man';

other = {
    height:1.7
}

Boolean

boolean 类型的值有两个:true(真)、false(假),虽然只有两个,但是 ECMAScript中所有类型的值与这个两个值都有等价关系,而想将其它类型的值转换为对相应的Boolean值,其实非常简单,只需要调用 Boolean() 方法即可。

Boolean('string');  // true
Boolean(0)          // false

其转换依据如下:

数据类型 转换为 true 转换为 false
String 不为空 为空('')
Number 不为0 0 或者 NAN
Object 任意对象 null
Undefined / undefined

掌握 Boolean() 方法的转换依据有助于我们明白JavaScript数据类型的隐式转换。

var str = 'string';
if(str) alert(true);

Number

简介

与其它语言不同的是,在ECMAScript中 Number类型只有一种并且不区分整数与小数。统一采用 IEEE754 标准,使用64位双精度浮点数来表示与存储。只是在运算的时候,会将可以转换为整数的小数以整数的形式进行计算,从而减少小数位数来更好的节省内存空间。

该标准不仅规定了浮点数的运算规则与表示格式,还规定了一些特殊值,例如正零(+0)与负零(-0)、无穷数(Infinity)以及非数值(NaN)等。

字面量格式

ECMAScript支持多种字面量格式,除了默认可以直接使用的十进制数,整数还可以通过八进制数以及十六进制的字面量来表示。

  • 八进制:(以8为基数,数码范围在:0 ~ 7) 前缀以 0 开头,如果超出数码的范围,则默认转换为十进制数来保存。
  • 十六进制:(以16基数,数码范围在:[0 ~ 9A ~ F] 不区分大小写),前缀以 0x 开头。
070  // 八进制   56(D)
080  // 十进制   80(D)
0x1A // 十六进制 26(D)

需要注意的是ECMAScript5中的严格模式并不支持八进制数。

虽然 ECMAScript 可以支持多种进制数,但是在运算的时候还会自动转换为对应的十进制数。
除了支持多种格式的数值字面量,ECMAScript还支持数值的一种表示方法 -> “科学计数法”。

M×R^E

M 是有效数字,R 是基数,默认为10,E 就是指数。

3125000 => 3.125×10^6 => 3.125E6
0.003 => 3×10^-3 => 3E-3

NaN
NaN 即非数值(Not a Number),用于表示返回一个非正确的数值结果。
NaN 特点的就是与任何类型的值进行运算,返回的结果仍然是 NaN。任何值与NaN 进行等价判断,返回的结果总会是 false
ECMAScript 提供了一个 isNaN 的方法,可以用来检测当前的值是否是一个 NaN 。是的话,返回true,不是则返回 false

isNaN(NaN)   // true
isNaN('123') // false
isNaN(true)  // false
isNaN('blue') // true

isNaN() 方法在具体的检测中也会根据需要进行值的类型转换。

精度与范围

由于内存容量的限制,以及按照IEEE754规定使用64位二进制位来保存浮点数,因此在 ECMAScript中浮点数的表示范围与精度都是有限的,不可能存的下这个世界上所有的值。

先观察下面这个计算:

0.1 + 0.2 // 0.30000000000000004

运算的结果超出我们的预料,原本以为结果是 0.3,实际上的结果却是 0.30....4,我们先抛开结果不谈,就说下通过观察这个计算,而得出的一些结论:

  • 0.300..4 小数点的位数只有17位,这说明了ECMAScript最多只能保持17位小数的精度,超出部分会被舍去。
  • .300.4 结尾是4,结合第一条超出17位会被舍去的结论,这里肯定不仅发生舍去还发生了进位。
  • 是否从第一条与第二条结论中,推导出 ECMAScript底层进行数值运算的时候 0.10.2 在转换的时候发生了无限不循环的情况呢?

经过查证,我们可以得知在很多编程语言(包括ECMAScript)其底层的运算都是基于二进制补码的形式来计算的,所以我们将 0.10.2 转换为对应的补码:

0.1[补] = 0.0001 1001 1001 1001 1001 1001 1001 1001 …(1001无限循环) 
0.2[补] = 0.0011 0011 0011 0011 0011 0011 0011 0011 …(0011无限循环) 

接着我们再大致了解下 IEEE754的存储格式,IEEE754会使用双精度(64bit)来存储浮点数,其中符号位占据1位,指数位占据11位,而尾数部分则占据52位,具体如下图所示:

ieee754.png-14.4kB

这也就直接说明了 0.10.2 在底层转换为补码的时候,就会因为无限循环而最多只能保存53位尾数的情况下发生了舍去进位,从而丢失精度。而且 0.10.2 在进行求和的时候,其结果还会再进一步舍去进位,丢失精度,最终在JavaScript中以最多17位小数的形式输出丢失精度的结果。

另外,需要注意的是,IEEE754还规定了,有效数字第一位默认总是1。因此表示精度的尾数前面,还存在着一个隐藏位,固定为1,它不包含在64位浮点数长度之中,也就是说尾数的格式总是:1.xx...xx 的格式,其中 xx...xxx 保存在64位长度之中,最长为52位,所以 JavaScript提供的有效数字(尾数)实际最长为53个二进制位。

这是为什么 Number.MAX_SAFE_INTEGER 的值是 9007199254740991Number.MIN_SAFE_INTEGER-9007199254740991 这是因为受限与尾数的位数,其数值可表示的安全范围则是: [-253-1,253-1]。

Number.MAX_SAFE_INTEGER === Math.pow(2,53)-1  //true
Number.MIN_SAFE_INTEGER === -Math.pow(2,53)+1 //true

但是实际上,由于尾数前面的隐藏位总为1,所以只要其范围在 (2^52)-1 < x <= (2^53)-1 之内 都可能会出现精度不准的情况。也就是说JavaScript中数值最安全的范围是在 [-(2^52)-1,(2^52)-1] 范围内。

接着再说下JavaScript的最大值与最小值,如果说数值的精度是由尾数的数量来决定的话,那么数值的大小或者说范围就是由指数来决定,IEEE754中,指数的长度是11位,因此指数部分能表示的最大指数就是 2^11-1 即 2047,然后根据规定再取中间值进行偏移,用来表示负指数,也就是说指数的范围是 [-1023,1024]之间,因此,这种存储结构能够表示的数值范围为 2^10242^-1023 ,超出这个范围的数JavaScript便无法表示,只能返回 Infinity,Infinity 根据值的正负情况具有 +Infinity-Infinity
2^1024 转换为科学计数法如下所示:

2^1024  = 1.7976931348623157 × 10308 === Number.MAX_VALUE === 1.7976931348623157e+308

也因此最小值为 Number.MIN_VALUE = 5e-324 既无限接近于 0。
总的来说,对于JavaScript中Number类型有效值的范围以及最大最小值可以通过Number对象的以下属性获得:

Number.MIN_VALUE //最小值
Number.MAX_VALUE //最大值
Number.GATIVE_INFINITY //负极限
Number.POSITIVE_INFINITY //正极限

也可以通过以下的方法来判断当前的值是否超出ECMAScript的可表示范围:

Number.isFinite(num)

最后,受限于JavaScript对数值的精度与范围的问题,要尽量避免使用JS来运算和处理大数值数据,如果必须要处理的话,也请使用一些第三方的数学库,来规避这些问题,这里推荐的数学库有:Math.js、decimal.js、big.js。

数值转换
在 ECMAScript中有三个方法可以显示的将其它类型的值转换为对应的数值型,这三个方法分别是:Number()parseInt()parseFloat()
Number()方法是对 Number 对象的静态调用,它支持将多种数据格式转换为对应的数值类型,其转换的主要规则(凭据)如下:

  • undefined 类型会转换为 NaN
  • null 类型会转换为 0,空字符串也会转换为 0
  • 对于字符串,可以将字符串形式的数值转换为对应的数值数据。(字符串形式的数值指的是只含有数值(0-9),正负号(+、-),小数点等字符的字符串),如果字符串中还含有其它特殊字符,则使用 Number() 方法会返回 NaN。需要注意的是如果字符串的开头与结尾含有空格,那么 Number() 会忽略前导与后导空格,但并不会忽略其它部位的空格。
  • 对于布尔型,则会将 true 转换为1 false 转换为 0。
  • 对于数值可以直接转换,对于十六进制或者是八进制的数值,则会转换为对应的十进制(需要注意的是严格模式下不支持八进制)。
  • 对于对象,则调用对象的 valueOf() 方法返回对象的值,如果返回的值仍然是一个引用类型而不是基本类型的值,那么再次调用对象的 toString() 方法,最后再转换。
Number(' 123 ')   // 123
Number(0xla);     // 26
Number('')        // 0
Number('123abc')  // NaN

相比较 Number()方法,parseInt() 以及 parseFloat() 的转换规则要简明的多,只要是符合字符串形式的数值(正负号、小数点,数值0-9)都可以使用parseInt()parseFloat() 转换为对应的十进制数,只是需要注意的是, parseInt() 方法并不识别小数点。

parseInt("123");        //123
parseInt("+123")        //123
parseFloat('-123.123'); //-123.123

parseInt()parseFloat() 在转换字符串形式的数值时,如果遇到特殊的字符,则会停止转换,并返回之前符合条件的值,而如果转换的值是其它类型的值,则返回NaN。

parseInt('123.123');    //123
parseInt('1abc123');    //1
parseFloat(true)        //NaN

相比较parseFloat()parseInt() 在转换的时候可以支持将八进制、十六进制转换为对应的十进制。

parseInt(070)  //56
parseInt(0x1a) //26

parseInt() 还具有第二个参数,用来指定当前进行转换的数的进制,从而在转换的时候无需为被转换的数添加所需的前缀。

parseInt(70,8) //56
parseInt('1a',16) 26

同样在ES5严格模式下不支持八进制。

String

简介
String 类型的值就是指由零个或多个使用16位unicode字符保存的字符序列,即字符串。

转义字符
String 类型的值除了非常普遍的 unicode字符序列,它还有一些具有特殊含义的字面量,比如“转义字符”

转义字符 含义
\n 换行
\t 制表符
\b 退格
\r 回车
\f 分页
\[*] 反转义 ,即不转义。例如:\\ 就是输出斜杠,' 输出单引号
\xnn 使用十六进制编码来表示一个字符。例如\x41 就是表示 "A"
\unnnn 使用四位unicode编码来表示一个字符。例如 \u03a3 表示希腊字母Σ

实际上通过仔细观察可以发现,转义字符都会以斜杠(\) 开头。

字符串转换
将其它数据类型的值显示的转换为 “字符串” 可以直接使用每种数据类型都会自带的 toString() 方法。

var num = 123;
var boolean = true
var obj = {}; 
 
num.toString() // "123";
boolean.toString() // "true";
obj.toString() //"[object Object]";

需要注意的是如果是将数值转换为字符串,toString() 方法还可以接受一个参数,即转换为对应的进制数的字符串。

var num = 10;
num.toString(2);  //"1010"
num.toString(8);  //"12"
num.toString(16); //"a"

如果不带参数,默认情况下就是直接转换为十进制。
为什么每种数据类型都会自带 toString() 方法呢?实际上道理很简单,那就是 ECMAScript中万物皆对象,对象中自带了 toString 方法,而别的数据类型则也是继承至对象,所以便与生俱来自带有 toString 方法。
很明显的是像 nullundefined 这两个类型明显不是派生于对象,所以它们并不具有 toString 方法。

var nl = null; 
var un = undefined;

nl.toString(); //Cannot read property 'toString' of null
un.toString(); //Cannot read property 'toString' of undefined

在此种情况下如果想将 nullundefined 再通过对象自带的 toString() 方法转换为字符串明显是行不通的了。
解决的方法也很简单,那就是与 Number 类型一样,使用 ECMAScript 自带的静态 String() 方法。

String(null); // "null"
String(undefined); // "undefined"

所以在进行字符串转换的时候,为了稳妥起见还是尽可能的多使用 String() 方法。这是因为String() 方法遵循以下转换规则:

  • 如果被转换的值具有toString() 方法,则调用其 toString() 方法。
  • 如果是 null 则转换为 "null"。
  • 如果值是 undefined 则转换为 "undefined"。

但是使用者需要明白的是,并不是所有的引用类型都会继承至objecttoString()方法,也会具有适合自己的toString()方法

'123'.toString() // '123'

(255).toString(16) // 'ff'
Number(255).toString(16) //'ff'

[].toString() // ""
[1,2,3].toString() // "1,2,3"

({}).toString() //"[object Object]"

当然,你也可以统一的使用某个对象的 toString() 方法来保持统一的转换结果。

Object.prototype.toString.call({})      //"[object Object]"
Object.prototype.toString.call([])      //"[object Array]"
Object.prototype.toString.call("")      //"[object String]"
Object.prototype.toString.call(1)       //"[object Number]"
Object.prototype.toString.call(true)    //"[object Boolean]"
Object.prototype.toString.call(null)    //"[object Null]"
Object.prototype.toString.call(undefined)//"[object Object]"

Object

对象类型,也可以称之为“枚举”类型或者是“引用”类型,它是数据与功能的集合,其中数据就是属性,而功能就是方法。
在ECMAScript中 Object 对象是其它所有对象的基础,因此Object 对象自带的一些属性方法也必然也会被它的实例化对象所继承。下面就是 Object 对象自带的一些常用属性以及方法:

constructor
保存着创建当前对象实例的构造函数。

new Object().constructor; // "ƒ Object() { [native code] }" 

这说明当前对象的构造函数就是 Object()

hasOwnProperty
该方法接收一个字符串作为参数,这个字符串就是 key,判断这个key是否在该对象的实例中,而非继承至原型。

var obj = new Object();

obj.custom = 1;
obj.hasOwnProperty('custom'); // true
obj.hasOwnProperty('custom1'); // false

isPrototypeOf(obj)
用于检查传入的对象是否是当前对象的原型上。

function A(){};
var a = new A();
console.log(A.prototype.isPrototypeOf(a)); //true

propertyIsEnumerable(propertyName)
检查当前对象上指定的属性是否可枚举(for...in循环遍历)。

function A(){
    this.custom = 1;
};

var a = new A();
a.propertyIsEnumerable('custom'); //true
a.propertyIsEnumerable('custom1'); //false

toString()
返回对象的字符串值。

valueOf()
返回对象的值,如果返回的值不是基本数据类型(数值,字符串,布尔值,null)则会继续调用 toString()方法返回对象的字符串值。

运算符

运算符就是运算符号,在ECMAScript中最神奇的就在于每个运算符都可以适用于多种类型的数据,为了避免程序运行错误,当一个运算符用作于一个不太适当的数据类型时,在运算之前,运算符会先隐式的进行数据类型转换。

自增/自减

  • 当应用于一个字符串形式的数值时,先将其转换为数值再进行自增自减操作。
  • 当应用于一个字符串但是非数值形式时,则转换为NaN
  • 当应用于一个 Boolean 值时,false转换为0,true转换为1,然后进行自增/自减操作。
  • 当应用于null或者是undefined时,null 会被转换为0,undefinedNaN
  • 当应用于一个对象(object)时,先调用对象的 valueOf() 方法,如果返回的值不是一个基本类型,则继续调用 toString() 方法,最后进行转换,再自增自减操作。
var str = 'a';
var str_num = '1.23';
var obj = {};

++str;
--str_num;
obj++

一元加减运算符

一元加减运算符非常符合我们对数学中的正号(+)与负号(-)的认识,只是在 ECMAScript中一元加减运算符除了能改变数值的正负以外,还会对不符合的数据类型进行数值型转换,其转换规则与调用静态方法 Number() 以及自增/自减运算符一致。

位运算符

简介

在ECMAScript所有的数值都是以 IEEE754标准采用64位二进制进行存储的,但是“位”运算符并不会对64位二进制值进行操作,而是会将64位转换为32位整数再进行运算,最终再将结果转换回64位保存,由于存储和转换的过程对使用者而言都是透明的,所以使用者只能关注到进行运算的32位整数。

在这个32位整数中,有一位为符号位,剩下的31位才是数值位,符号位决定了这个数在计算机中对应二进制数的存储格式,其中0表示正,1表示负,同一个数,如果符号为正,只需要存储其对应的二进制原码,如果为负,便需要将其转换为补码形式进行存储计算。

例如 +8 原码为:

8-ym.png-8.6kB

对于正数由于其原、反、补都是相同的,所以无需关注,而对于负数,则求其反码或者是补码会有些繁琐,主要经过以下步骤:

  1. 求其绝对值的原码,但注意符号位为1
  2. 求原码的反码(按位取反,符号位不变)
  3. 反码加1

例如 -8,求其补码为:

(1)- |8| = -1000 = 1...1000
(2) 取反:1...0001
(3) 加1:1...0010

由于ECMAScript的位运算符只能对32位整数进行运算,所以对于超出 2^31-1的数值(符号位占1位),在运算的时候都会可能出现失去精度的情况。

~~2147483647 // 2147483647
~~2147483648 // -2147483648
~~2147483649 // -2147483647
~~4294967296 // 0

非、与、或
非:~,二进制位(包括符号位)按位取反:~0...1001 => 1...0110。eg:~9 => -10
与:&,二进制位同1位1,不同为0。
或:|,二进制位有一个1就为1,只有同为0才会为0。

由于“位”运算符会将64位浮点数转换为32位整数,因此利用 ~~ 双按位反运算符便可以进行数值的快速取整,其功能类似于parseInt,只是这里进行的是位运算,速度更快。

异或
运算符:^
功能:二进制位不同为1,相同为0.
示例:

    101011100
   ^
    010001100
 ----------------
    111010000

左移位
运算符:<<
功能:将对应操作数的32位二进制数向左移动指定的位数,移动后,右侧空余的位置用0填充。
示例:

2 << 2  // 8
2 << 1  // 4
-2 << 5 // -64

需要注意的是“左移”并不会影响操作数的符号位,它移动的只是操作数的绝对值,但是结果符号的依然与操作数的符号相同。

-64 => -|-64| => -(|-64|<<2) => -256

如果用一组数学公式去计算左移的值的话,应该是:N*~~((2^s))。其中 N 是要移动的操作数,而 s 则是要左移的位数。

-64 << 2 == -64 * ~~(Math.pow(2,2)); //true

有符号右移
运算符:>>
功能:将对应操作数的32位二进制数向右移动指定的位数,移动后左侧空余的位置用操作数的符号位填充。

64 >> 2 // 16
-64 >> 2 //-16

无符号右移位
运算符:>>>
功能:将对应操作数的32位二进制数向右移动指定的位数,移动后左侧空余的位置用0填充。
示例:

64 >>> 2 //16

如果操作数是正数,那么无符号右移动,与有符号右移完全相同,但是如果操作数是负数,那么结果就会不同了

-64 >>> 5 //67108863

出现这个问题的原因,就是因为无符号右移运算符会把负数的二进制数再按照指定的位数右移后,却会以正数的二进制数形式转换为对应的十进制。从而导致结果非常大。

例如 -64的补码是:1111 1111 1111 1111 1111 1111 1100 0000 右移后的结果是 0000 0011 1111 1111 1111 1111 1111 1111 但是在转换为十进制数的时候,并没有减一取反,而是以正数的二进制数直接转换为10进制,所以结果为 67108863

逻辑运算符

非运算符
运算符:!
功能:“逻辑非”运算符会将操作数取反,最终返回一个布尔值。逻辑非在进行求反之前会调用 Boolean() 这个静态方法将非布尔型的值转换为布尔值,然后进行求反操作。
示例:

!NaN //true
!undefined //true
!null //true

利用逻辑取反运算符会返回布尔值的这一特性,通过使用双反运算来进行数据类型的转换。

!!NaN //false
!!undefined //false
!!null //false

与运算符

运算符:&&
功能:“逻辑与”是一个双目运算符,它可以链接多个操作数,并且操作数可以是任何的数据类型,并不一定是非要是布尔值,而且逻辑与运算符的运算结果也并非一定是布尔值,而是满足条件的那个结果。
说明:逻辑与存在短路的情况,即只要第一个操作数不为“真(true)”,那么后面的条件就会被忽略。

false && a // false

这里变量 a 并未定义,但是再进行逻辑与运算的时候,并不会报错,这是因为在逻辑短路的情况下,后面的代码就不会被执行。
对于逻辑与而言,只有当第一个操作数的结果为真的时候,才会向下执行,最终返回符合条件的那个操作数的值,但是一旦遇到值为“假”的操作数,就会中断执行。

true && false && 1 // false
NaN && 'true' //NaN
true && 2 //2

或运算符

运算符:||
功能:“逻辑或”是一个双目运算符,它可以链接多个操作数,并且操作数可以是任何的数据类型,并不一定非要是布尔值,而且逻辑或运算符的运算结果也并非一定是布尔值,而是满足条件的那个结果。
说明:逻辑或存在短路的情况,即只要第一个操作数为“真(true)”,那么后面的条件就会被忽略。

true || a //true

这里变量 a 并未定义,但是在进行逻或与运算的时候,并不会报错,这是因为再逻辑短路的情况下,后面的代码就不会被执行。
对于逻辑或而言,只有当第一个操作数的结果为“假”的时候,才会向下执行,最终返回符合条件的那个操作数的值,但是一旦遇到值为“真”的操作数,就会中断执行。

true || false //true
false || true //true
false || false //false

算术运算符###

在算术运算符中,如果操作数的数据类型不一致,也会进行数据类型的隐式转换,只是不同的运算符转换的规则也有一定的不同:

乘/除/求余

  • 如果操作数都是数值,则进行常规的乘/除/求余操作,如果所得的结果超过ECMAScript可表示的范围,则返回Infinity
  • 如果一个操作数的结果是NaN,那么运算结果也将是 NaN
  • 如果有一个操作数是非数值类型的,则调用静态 Number() 方法将其转换为数值型,然后进行乘/除/求余操作。
  • 如果零除以零,则结果为 NaN
  • 如果一个常规的数值被零除,则结果也为 Infinity
1*NaN //NaN
1/0   //Infinity
0/0  //NaN

加减

  • 如果操作数都是常规的数值,则进行常规的操作,如果所得的结果超过ECMAScript可表示的范围,则返回Infinity
  • 在进行加法运算时,如果有一个操作数是字符串,则将另一个操作也转换为字符串然后进行拼接,如果两个操作数都是字符串则直接进行拼接操作。
  • 在进行加法运算时,如果有一个操作数是数值,另一个操作数是 nullundefined、布尔值等其中任意一个,则调用Number() 静态方法将它们转换为对应的值后进行运算。
  • 任何数与NaN 进行 运算,结果依然为 NaN
  • 如果有操作数是一个引用类型,则调用它的 valueOf() 方法获取这个引用类型的基本值,如果值仍然不是一个基本数据类型,则再调用 toString() 方法。
1 + 'a' //'1a'
'a' + 'b' //'ab'
1 + null //NaN
1 + true //2
1 + undefined //NaN
1 + NaN //NaN
1 + {} // "1[object Object]"

减法

  • 如果操作数都是常规的数值,则进行常规的操作,如果所得的结果超过 ECMAScript可表示的范围,则返回Infinity
  • 任何数与NaN 进行 运算,结果依然为 NaN
  • 如果操作数中有字符、布尔型、nullundefined中的任意一种,在进行减法运算之前,都会先调用 Number() 方法将它们转换为对应的数值,最后再进行减法运算。
  • 如果操作数中有引用类型,则调用它的 valueOf() 方法获取这个引用类型的值,如果值仍然不是一个基本数据类型,则再调用 toString() 方法,接着再使用 Number() 方法将引用对象返回的值再转换为数值,最后再进行减法运算。
1 - null //NaN
1 - undefind //NaN
1 - true //0
1 - NaN //NaN
1 - {} //NaN
'A' - 'b' //NaN

比较运算符###

比较运算符的返回结果是一个布尔型(boolean)。
比较运算符有:> 、>= 、< 、<= 、!= 、!、===。
比较运算符在计算的时候,也会根据值的不同而不同。
在同类型值的情况下:

  • 数值之间直接进行大小比较。
  • 字符串之间按字符顺序进行每个字符的 Ascii码进行比较。
  • 布尔值比较,true 大于 false

如果所比较的值类型并不相同,那么便需要进行一定的隐式转换之后方能进行比较:

  • 数值与字符串之间比较,会将字符串转换为数值,如果不是标准的字符串形式数值,则返回 NaN
  • 当比较的值中含有 NaN 时,除了不相等运算符外任何比较运算符的结果都是 NaN
  • 当比较的值中含有 nullundefind 时,null 会转换为数值0,而 undefind 转换后为 NaN
  • 如果比较的值中有 false 转换为数值0,如果有 true 转换为数值1。
  • 如果比较的值中含有“引用类型”时除了相等运算符外,都会调用该引用对象的 valueOf 方法返回对象的值,如果返回的值依然不是一个基本数据类型,则再进一步调用 toString() 方法,最后再根据值的数据类型是否一致从而分别应用上述规则。如果对象进行相等性检测,则会判断是否具有相同的引用。

var obj = {
    valueOf: function() {
        return 1;
    }
};

console.log(2 > obj);  //true
console.log(null > 0)  //false
console.log(null >= 0) //true
console.log(null < 0)  //false
console.log(null <= 0) //true

在比较运算符中,还有一个非常特殊的地方,那就是相等性与不相等性的比较运算符。

== //相等
!= //不相等

=== //严格相等(全等)
!== //严格不相等(不全等)

“严格相等”、“严格不相等”与普通的“相等”运算符以及“不相等”运算符的最大区别在于前者在进行比较运算的时候,不会发生数据类型的隐式转换。

55 == '55' //true
55 === '55' //false

对于“严格相等”、“严格不相等”运算符实际上并没有什么好说的,无非就是数据类型不会发生转换(使得使用时对结果的预测更加的准确),因此这里主要着重说明下普通的相等运算符与不相等运算符:

  • 按照ECMAScript规定,在进行相等运算时 null == undefined 比较结果为 true(注意在严格相等比较中,返回 false 因为它们并不是相同的数据类型)。
  • NaN不相等 NaN 时,结果为 trueNaN != NaN
  • null 在进行相等运算时也不会进行类型转换:null == 0 //false,这个需要注意下。
  • 如果操作数中含有 “引用类型”时,进行相等性判断,则判断是否为相同的应用,如果是则返回 true或者返回 false
null == undefined //true
null === undefined //false

NaN != NaN //true
null == 0 //false

var a = {};
var b = a;
var c = [];

a == b //true
c == b //false

对于具体的开发,在进行相等性比较,强烈建议使用“严格相等”与“严格不相等”运算符,避免因数据类型的隐式转换而产生错误。

条件运算符###

条件运算符是 ECMAScript中唯一的一个三目运算符

conditaion ? exp1 : exp2

赋值运算符

需要注意的是一些不常见的复合赋值运算符

<<= 、>>= 、>>>=、%=

逗号运算符

逗号运算符主要有两个作用:

  • 表达式的分隔,例如同时声明多个变量
var a,b,c;
  • 返回表达式的最后一项值
var num = (1,2,3,4); // 4;

表达式与语句##

“表达式”通常指的是用运算符号将操作数链接起来可能会产生运算结果的式子。而“语句”则以功能或者行为为基本单位,用来完成某个给定的任务或者是操作,并常以分号为结尾。平常也称“语句”为指令或指令的集合。

表达式语句###

知道“语句”的定义与概念,所以表达式加上分号 ; 也可以称之为表达式语句,再详细的划分有:

  • 算术表达式语句
  • 移位表达式语句
  • 逻辑表达式语句
    ...

除了众多的表达式语句,在ECMAScript中使用关键字来定义的语句还有“条件语句”、“循环语句”、“函数语句”等。

循环语句###

在ECMAScript中组成循环语句的关键字是有:whileforindo 等。
这些关键字构成的循环语句主要有以下类别:

while(){..} 

do{}while()

for(){..}

for..in

其中 for 循环是最常用到的,其详细格式如下:

for(init;condition;loop_express) statement

其中的 init 表示初始化一个变量,这个变量也可以在for循环外定义,但是不论是外还是内,因为 ECMAScript5之前都不存在块级作用域,所以循环体的上下文都可以访问到这个变量。
condition 是循环的条件,只有为 true 时才会进入循环体,而 loop_express 则是用于控制循环条件状态的变更。

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

for 循环中,initloop_express 可以留空,在别的地方定义

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

当然,你也可以三个参数都不要,这样实际上就成为了一个死循环

for(;;){}

但是还是强烈建议在定义 for 循环的时候参数都尽量都写在循环之中。
在具体的使用上,init 不仅可以用于初始化变量,还可以是一些列用逗号 , 隔开来同时执行的表达式。

for(var i=0,j=10,m=20;i<20;i++){...}

除了 for 循环外,常用的就数 for..in 循环了,这两者的区别,for 循环可以更好的对数组进行遍历,而 for..in 则可以方便的枚举对象的成员。

标签语句###

格式:

label:statements

需要注意的是 label 并不是关键字,而是代指“标识符”。
“标签语句”常用于与 breakcontinue 等关键字结合使用,在代码执行的时候跳转到程序的指定位置。
“标签语句”在具体的使用中,常用于多层循环体的内部循环,实现退出整个循环的功能(因为单纯的 break 只能退出当前循环,而无法在多层的循环内部直接中断所有循环)。

wrap:
    for(var i=0;i<10;i++){
        for(var j=0;j<10;j++){
            if(j==5 && i==5) break wrap;
        }
    }

with语句###

将指定的对象加入到当前作用域链的源头,从而实现快速的访问对象中的某个成员。

with(location){
    console.log(href)
}

with 语句虽然可以提供对象成员的便捷访问,但也会导致歧义的产生,如在 with 语句体外定义了一个变量与当前对象的成员重名,便会使自定义的变量无效(当重名时,with语句优先级使用对象的同名方法或属性)。

var href = {};
with(location){
  console.log(href);
}

另外,大量使用 with 语句还会导致性能的下降,并且ES5标准已经规定严格模式不能使用 with 语句。

switch语句###

switch 语句与 if 语句关系很紧密,一般来说,当需要判断的条件在三个以上的时候,推荐使用 swtich 语句,否则建议使用“条件语句”,下面是 switch 语句的具体格式:

switch(expression){
    case value :
        statements;
        break;
    default:
        statements
}

表达式(expression)与每种情况(case)的值(value)可以是任何数据类型的值,不像其它语言中只能是数值,在 ECMAScript中 表达式与 case的值不仅可以是固定的常量,还允许是一个变量:

var a = 'a';

switch("a"){
    case 1:
        break;
    case a:
        break;
    default:
}

case 除了数据类型可以不固定外,还可以是一个表达式,当然匹配的结果就是与表达式的值进行匹配。

var num = 20;
switch(true){
    case num < 0 : break;
    case num > 5 && num < 10 : break;
    case num == 10 :break ;
    default:
}

switch语句内部对 expressioncasevalue 来进行判断与匹配时,都是以“全等”的方式来进行判断的,所以 switch中值的匹配时不会发生数据类型的转换。

switch(10){
    case '10':break;
    defaut:
}

最后还有一个很实用的技巧,如果一个 switch 语句定义在某一个函数的内部,那么可以使用 return 来代替 break,因为 return 在结束函数执行的过程中也就中断了switch语句的执行。

函数语句###

在ECMAScript中如果函数没有明确指定返回值,那么默认返回 undefined

函数参数

与其它语言一样,ECMAScript中函数的参数也有两种,分别是声明函数时用来接收值的“形参”以及调用函数时传输的“实参”。
继承至ECMAScript弱类型的特点,形参与变量的性质实际上是相同的,可以接收任何类型的值,并且形参还不会像变量那样有作用域提升的情况。

相比与其它语言,ECMAScript中函数参数的传输,不是以单个单个参数的形式传输,而是整个以数组的形式进行传输,也就是说无需关注每次传输时参数的具体数量(形参与实参是否一致)。

关于这点,我们可以在函数体中访问 arguments 对象便一目了然

arguments.length // 获取传入参数的数量
arguments[0];
arguments[1];
...

arguments 虽然类似与数组,但并不是真的数组,访问其原型链,可以发现继承至 object

arguments.__proto__

对于这些类似与数组,而又非数组的值,我们可以很形象的称之为 Like Array
到了这里,我们就可以明白,实际上函数的形参只是帮助我们更好的理解与书写代码,而非必需要有的,在使用 arguments 修改某个参数的值,其对应的形参也会发生改变:

arguments[0] = 'test';
console.log(a) 

但这并不能说明形参就是对 arguments 中某个元素对象的引用,实际上它们俩的内存空间完全是独立的,只是它们的值会具有相互绑定的关系,从而实现相互更新值的特性。我个人认为这是因为 arguments 毕竟是一个对象,在函数体中频繁的访问或修改对象的成员,对于性能的影响也是不可忽略的。

通过 arguments 除了可以访问函数的参数,获取参数的数量,它还有一个非常有用的方法 callee,它与 this类似可以对自身进行引用,因此常用这个方法实现函数的迭代执行。

function factorial(num){
    if (num <= 1)
        return 1;
    else
        return num * arguments.callee(num - 1);
};

需要注意的是ECMAScript严格模式下 callee 将无法使用。

posted @ 2018-06-01 12:43  卷柏的花期  阅读(647)  评论(2编辑  收藏  举报