类型
使用复合条件检测null的类型
var a = null;
console.log(!a && typeof a === 'object');
函数是'可调用的对象', 是object的一个'子类型', 它有一个内部属性[[Call]], 该属性使其可以被调用
�函数对象的length属性是其声明的参数个数
function a(b, c) {
}
console.log(a.length); // 2
变量是没有类型的,只有值才有;typeof 得到的结果时该变量持有值得类型,因为JavaScript中变量没有类型
typeof 运算符总是会返回一个字符串
console.log( typeof typeof 42); // 'string'
undefined(已在作用域中声明但是还没赋值的变量): 变量在未持有值得时候
undeclared(还没有在作用域中声明过的变量):
var a;
console.log(a); // undefined
console.log(b); // ReferenceError: b is not defined
typeof a; // undefined
typeof b; // undefined b是一个undeclared变量,没有报错,是因为typeof 有一个特殊的安全防范机制
undefined
和is not defined
是两码事
使用typeof 的安全防范机制(阻止报错)来检查undeclared变量
// 这样做会抛出错误 ReferenceError: b is not defined
if(b) {
}
// 这样是安全的
if(typeof b !== 'undefined') {
console.log(11);
}
值
数组
var a = [1, '2', [3]];
console.log(a.length, a); // 3 [ 1, '2', [ 3 ] ]
delete a[0];
console.log(a.length, a); // 3 [ <1 empty item>, '2', [ 3 ] ]
使用delete运算符可以将单元才能够数组中删除,但是删除后,数组的length不会发生变化 ?
'稀疏'数组(含有空白或空缺单元的数组)
var a = [];
a[0] = 1;
a[2] = [3];
console.log(a.length, a[1]); // 3 undefined
a[1]的值是undefined, 这将与将其显式赋值为undefined(a[1] = undefined) 还是有区别的 ?
类数组
有时候需要将类数组(一组通过数字索引的值)转化为真正的数字;
一般通过 数组工具函数(如indexOf(..), concat(..), forEach(..)等)来实现
工具函数slice(..)经常被用于这类转换
function foo() {
var arr = Array.prototype.slice.call(arguments);
arr.push('bam');
console.log(arr); // [ 'bar', 'baz', 'bam' ]
};
foo('bar', 'baz')
es6的内置函数 Array.from(..)也能实现同样的功能
...
var arr = Array.from(arguments);
...
字符串
字符串经常被当做字符数组, 它们都是类数组,都有length属性以及 indexOf(..)和concat(..)方法;
JavaScript中字符串是不可变的,但是数组是可变的;
字符串不可变是指字符串的成员函数不会改变其原始值,而是创建并返回一个新的字符串.而数组的成员函数都是在其原始值上进行操作;
var a = 'foo';
c = a.toUpperCase();
console.log(a === c); // false
console.log(a, c); // foo FOO
许多数组函数用来处理字符串很方便,虽然字符串没有这些函数,但是可以通过'借用'数组的非变更方法来处理字符串
var a = 'foo';
var c = Array.prototype.join.call(a, '-');
console.log(c); // f-o-o
var d = Array.prototype.map.call(a, function(v) {
return v.toUpperCase() + '.';
}).join('');
console.log(d); // F.O.O.
另一个不通电在于字符串反转,数组有一个字符串没有的可变更成员函数reverse();
我们无法'借用'数组的可变成员函数,因为字符串是不可变的
一个变通(破解)的方法是先将字符串转换为数组,待处理完后再讲结果转换为字符串
var c = a
// 将a的值转换为字符数组
.split('')
// 将数组中的字符进行倒转
.reverse()
// 将数组中的字符拼接会字符串
.join('');
数字
42.toFixed(2); // SyntaxError
下面的语法都有效
(42).toFixed(3);
0.42.toFixed(3);
42..toFixed(3);
42.toFixed(3)是无效语法,因为.被视为常量42.的一部分;42..toFixed(3)则没有问题,因为第一个.被视为number的一部分,第二个.是属性访问运算度;
较小的数值
整数安全范围
Number.MAX_SAFE_INTEGER
Number.MIN_SAFE_INTEGER
有时JavaScript程序需要处理一些比较大的数字,如数据库的64位ID等,由于JavaScript的数字类型无法精确呈现64位数值,所以必须将他们保存转换为字符串
整数检测
Number.isInteger();
Number.isSafeInteger();
特殊数值
null, undefined,
null是一个特殊关键字,不是标识符,我们不能讲其当做变量来使用和赋值.然而undefined却是一个标识符,非严格模式下可以被当做变量来使用和赋值
void运算符
undefined是一个内置标识符,它的值为undefined,通过viod运算符即可得到该值;
表达式viod没有返回值,因此返回结果是undefined,void并不改变表达式的结果,只是让表达式不返回值
var a = 42;
console.log(void a, a); // undefined 42
习惯上用void 0
来获得undefined
function doSomething() {
if(!App.ready) {
// 不让表达式返回结果
return void setTimeout(doSomething, 100);
}
var result;
return result;
}
if(doSomething()) {
//
}
类似于
if(!App.ready) {
setTimeout(doSomething, 100);
return;
}
总之,如果要将代码中的值(如表达式的返回值)设为undefined,就可以使用void
特殊数字
1.不是数字的数字NaN
如果数学运算的操作数不是数字类型,就无法返回一个有效的数字,这种情况下返回值为NaN;
NaN是一个特殊值,它和自身不相等,是唯一一个非自反(自反, 即 x===x不成立)的值, NaN != NaN 为true
isNaN(..)判断一个值是否为NaN
但是有个验证缺陷, 检查方式过于死板,就是'检查参数是否不是NaN, 也不是数字';
var a = 2 / 'foo';
var b = 'foo';
console.log(a);
console.log(window.isNaN(a), window.isNaN(b)); // true true
es6提供了 Number.isNaN();
实际还有一个更简单的方法,即利用NaN不等于自身这个特点.NaN是JavaScript中唯一一个不等于自身的值;
if(!Number.isNaN) {
Number.isNaN = function (n) {
return n !== n;
}
}
2.无穷数
var a = 1/0; // Infinity
var a = -1/0; // -Infinity
Infinity/Infinity; // NaN
javascript的运算结果有可能溢出, 此时的结果为 Infinity或-Infinity
3.零值
4.特殊等式
NaN和-0在相等比较时有些特别, NaN和自身不相等,使用Number.isNaN(..); -0等于0, 使用isNegZero(..)
Object.is(..)来判断两个值是否绝对相等;
能使用 == 或者 === 时就尽量不要使用Object.is(..);因为前者效率更高,更为通用,后者主要用来处理那些特殊的相等比较
值和引用
简单标量基本类型值(字符串和数字)通过值复制来赋值/传递,而复合值(对象等)通过引用复制来赋值/传递;JavaScript中的引用和其他语言中的引用/指针不同,他们不能指向别的变量/引用,只能指向值
function foo(x) {
x.push(4); // x:[ 1, 2, 3, 4 ]
x = [4, 5, 6];
x.push(7);// x:[ 4, 5, 6, 7 ]
}
var a = [1, 2, 3];
foo(a); // a:[ 1, 2, 3, 4 ]
function foo(x) {
x.push(4); // x:[ 1, 2, 3, 4 ]
x = [4, 5, 6];
x.length = 0;
x.push(4, 5, 6, 7); //a:[ 4, 5, 6, 7 ]
}
var a = [1, 2, 3];
foo(a); // a:[ 4, 5, 6, 7 ]
x.length = 0和x.push(4, 5, 6, 7)并没有创建一个新的数组,而是更改了当前的数组,所以a的值变成了[ 4, 5, 6, 7 ]
如果通过值复制的方式来传递复合值
slice(..)不带参数会返回当前数组的一个浅复本,由于传递给函数的是指向该复本的引用, 所以foo(..)中的操作不会影响a指向的数组
原生函数
常用的原生函数
- String()
- Number()
- Boolean()
- Array()
- Object()
- Function()
- RegExp()
- Date()
- Error()
- Symbol()
内部属性[[Class]]
所有typeof返回值为'object'的对象(如数组)都包含一个内部属性[Class].这个属性无法直接访问,一般通过Object.prototype.toString(..)
来查看
Object.prototype.toString.call([1, 2, 3]); // "[object Array]"
封装对象包装
var a = 'abc';
var b = new String(a);
var c = Object(a);
console.log(typeof a); // string
console.log(typeof b); // object
console.log(typeof c); // object
console.log(b instanceof String); // true
console.log(c instanceof String); // true
console.log(Object.prototype.toString.call(b)); // [object String]
console.log(Object.prototype.toString.call(c)); // [object String]
拆封
如果想要得到封装对象中的基本类型值,可以使用valueOf()函数
var a = new String('abc');
var b = new Number(42);
var c = new Boolean(true);
console.log(a.valueOf()); // abc
console.log(b.valueOf()); // 42
console.log(c.valueOf()); // true
隐式拆封(强制类型转换)
var a = new String('abc');
var b = a + '';
console.log(typeof a); // object
console.log(typeof b); // string
将原生函数当做构造函数
Array构造函数只带一个数字参数的时候,该参数就会被当做数组的预设长度(length), 而非只充当数组中的一个元素
var a = new Array(3);
var b = [undefined, undefined, undefined];
var c = [];
c.length = 3;
console.log(a.length, a); // 3 [ <3 empty items> ]
console.log(b); // [ undefined, undefined, undefined ]
console.log(c); // [ <3 empty items> ]
console.log(a.join('-')); // --
console.log(b.join('-')); // --
a.map(function(v, i) {
console.log(i);
return i;
});
b.map(function(v, i) {
console.log(i);
return i;
})
a.map(..)之所以执行失败,是因为数组中并不存在任何单元,所以map(..)无从遍历, 而join(..)却不一样,具体实现可参考下面代码:
function fakeJoin(arr, connector) {
var str = '';
for(var i = 0; i < arr.length; i++) {
if(i > 0) {
str += connector;
}
if(arr[i] !== undefined) {
str += arr[i];
}
}
return Storage;
}
var a = new Array(3);
console.log(fakeJoin(a, '-'));
join(..)首先假定数组不为空,然后通过length属性值来遍历其中的元素,而map(..)并不做这样的假定
var a = Array.apply(null, {length: 3});
console.log(a); // [ undefined, undefined, undefined ]
apply(..)是一个工具函数,适用于所有函数对象,它会以一种特殊的方式来调用传递给它的函数
第一个参数是this对象,第二个参数则必须是一个数组(或者类似数组的值, 也叫做类数组对象, array-like object), 其中的值被用作函数的参数;
Object(..), Function(..)和RegExp(..)
除非万不得已,尽量不要使用Object(..), Function(..)和RegExp(..)
建议使用常量形式来定义正则表达式, 这样不仅语法简洁,执行效率也更高;因为JavaScipt引擎在代码执行前会对它们进行预编译和缓存,与其他构造函数不同,RegExp(..)还是很有用的,比如动态定义正则表达式
Date(..)和Error(..);
Date.now();
if(!Date.now) {
Date.now = function() {
return (new Date()).getTime();
}
}
Date()不带 new 关键字, 则会得到当前日期的字符串值
console.log(Date());
console.log(new Date());
// Wed May 30 2018 16:48:20 GMT+0800 (CST)
// 2018-05-30T08:48:20.089Z
symbol(..)
原生原型
其他构造函数的原型包含它们各自类型所持有的行为特征;比如Number#toFixed(..)
(将数字转换为指定长度的整数字符串)和Array#concat(..)
(合并数组), 所有的函数都可以调用Function.prototype中的apply(..), call(..)和bind(..)
小结
1.Javascript为基本数据类型提供了封装对象,称为原生函数(如 String, Number, Boolean等).它们为基本数据类型提供了该子类型所持有的方法和属性(如: String#trim() 和 Array#concat())
2.对于简单标量基本类型之,比如'abc', 如果要访问它的length属性或者String.prototype方法,JavaScript引擎会自动对该值进行封装(即使用相应类型的封装对象来包装它)来实现对这些属性和方法的访问;
强制类型转换
1.toString()
JSON字符串化
工具函数JSON.stringify(..)在将JSON对象序列化为字符串时也用到了toString();
console.log(JSON.stringify(undefined)); // undefined
console.log(JSON.stringify(function() {})); // undefined
console.log(JSON.stringify(
[1, undefined, function(){}, 4] // [1,null,null,4]
));
console.log(JSON.stringify(
{
a: 2,
b: function (){}, // {"a":2}
}
));
2.toNumber()
true转换为1, false转换为0, undefined转换为NaN, null转换为0;
对象(包括数组)会被首先转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字;
为了将转换为相应的基本类型值,抽象操作ToPrimitive会首先(通过内部操作DefaultValue)检查该值是否有valueOf()方法,如果有并且返回基本类型值,就使用该值进行强制类型转换,如果没有就使用toString()的返回值(如果存在)就进行轻质类型转换;
如果valueOf()和toString()均不返回基本类型值,会产生TypeError错误
使用Object.create(null)创建的toString()方法,因此无法进行轻质类型转换
3.toBoolean
以下都是假值
- undefined
- null
- false
- +0, -0和NaN
- ""
假值列表之外的值就是真值
[], {}和function(){}, 都是真值;
字符串和数字之间的转换
String(..),遵循前面讲过的ToString规则,将值转换为字符串基本类型
Number(..)遵循前面讲过的ToNumber规则,将值转换为数字基本类型
var a = 42;
var b = a.toString();
var c = '3.14';
var d = +c;
b; // "42"
d; // 3.14
a.toString()
, 也可以转为为字符串,但是其中涉及隐式转换.因为toString()对42这样的基本类型值不适用,所以JavaScript引擎会自动为42创建一个封装对象,然后对该对象调用toString().这里显式转换中含有隐式转换;
一元运算+被普遍认为是显示强制类型转换;
1.日期显式转换为数字
一元运算+另一个常见用途是将日期(Date)对象强制类型转换为数字,返回Unix时间戳,以微秒为单位(从1970年1月1日 00:00:00 UTC到当前时间);
var d = new Date('Wed May 30 2018 16:48:20 GMT+0800 (CST)');
+d;
// 常用下面的方法来获得当前的时间戳
var temestamp = +new Date();
奇特的~运算符 ?
~运算符(即字位操作符'非');
字位操作符只适用于32位整数,运算符会强制操作数使用32位格式,这是通过抽象操作ToInt32实现的
ToInt32首先会执行ToString强制类型转换,比如'123'先被转换为123,然后再执行ToInt32
字位运算符(如| 和~)个某些特殊数字一起使用会产生类似强制类型转换的效果,返回另外一个数字
console.log(0|-0); // 0
console.log(0|NaN); // 0
console.log(0|Infinity); // 0
console.log(0|-Infinity); // 0
以上这些特殊数字无法以32位格式呈现,因此toInt32返回0
~首先将值强制类型转换为32位数字,然后再执行字位操作'非'(对每一个字位进行反转)
字位截除
~~截除数字值的小数部分,
显式转换为布尔值
建议使用 Boolean(a) 和 !!a来进行显式强制类型转换
隐式强制类型转换
var a = [1, 2];
var b = [3, 4];
console.log(a+b); // 1,23,4
某个操作数如果是字符串或者能够通过以下步骤转换为字符串的话, +将进行拼接操作,如果其中一个操作数是对象(包括数组),则首先对其调用ToPromitive抽象操作,该抽象操作再调用[[DefaultValue]], 以数字作为上下文;
因为数组的valueOf()操作无法得到简单基本类型值,于是她转而调用toString().因此上例中的两个数组变成了'1, 2' 和'3, 4'。+将它们拼接后返回'1,23, 4'
var a = '3.14';
var b = a - 0;
console.log(b); // 3.14
-是数字减法运算符,因此a-0会将a强制类型转换为数字,也可以使用a*1和a/1,因为这两个运算符也只适用于数字
抽象相等
2 == [2]; // true
'' == [null]; // true
== 右边的值[2]和[null]会进行ToPromitive强制类型转换,以便能和左边的基本类型值(2和'')进行比较,因为数组的valueOf()返回数组本身,所以强制类型转换过程中数组会进行字符串化
[2]会转换成'2',然后通过ToNumber()转换为2;
[null]会直接转换为'';
所以最后的结果就是2 == 2和'' == ''
'', '\n'(或者' ' 等其他空格组合)等空字符串被toNumber强制转换为0;
'0' == false; // true
false == 0; //true
false == ''; // true
false == []; // true
'' == 0; // true
'' == []; // true
0 == []; // true
其中4中情况涉及 == false,之前我们说过应该避免,
现在剩下3种
'' == 0; // true
'' == []; // true
0 == []; // true
安全运用隐式强制类型转换
我们要对 == 两边的值认真推敲,以下两个原则可以让我们有效地避免出错
- 如果两边的值中有 true或者false,千万不要用 ==
- 如果两边的值中有 [], ''或者 0, 尽量不要使用 ==
抽象关系比较
1.比较双方首先会调用 ToPrimitive, 如果结果出现非字符串,就根据ToNumber规则将双方强制类型转为数字来比较
2.如果都是字符串,则按字母顺序来进行比较
3.<= 应该是 '小于或者等于', 实际上JavaScipt中 <= 是'不大于'的意思(!(a > b), 处理为!(b < a)).同理 a >= b 处理为 b <= a;
相等比较有严格相等,关系比较缺没有'严格关系比较', 如果要避免 a < b中发生隐式强制类型比较,我们只能确保 a和b为相同的类型,除此之外别无它法;
尽量使用 Number(a) < Number(b);
语法
关联
? : 是右关联
右关联不是指从右往左执行,而是指从右往左组合
a ? b : c ? d : e;
// 应该是以下组合顺序
a ? b : (c ? d : e);
某些情况, 返回的结果没有区别,但实际却有十分微妙的差别
var a = true, b = false, c = true, d = true, e = false;
a ? b : (c ? d : e); // false, 执行a和b
(a ? b : c) ? d : e; // false,执行a, b和e
另一个右关联的例子是 = 运算符
var a, b, c;
a = b = c = 42;
它首先执行 c = 42, 然后是b = .., 最后是a = .., 因为是右关联,所以它实际上是这样来处理的: a = (b = (c = 42));
来看一些复杂的例子
var a = 42;
var b = 'foo';
var c = false;
var d = a && b || c ? c || b ? a : c && b : a;
d; // 42
// 根据优先级和关联, 根据组合规则将上方代码分解如下
((a && b) || c) ? ((c || b) ? a : (c && b)) : a;
// 可以通过缩进显式让代码更容易理解
(
(a && b)
||
c
)
?
(
(c || b)
?
a
:
(c && b))
: a;
(1) (a && b) 结果为 'foo';
(2) 'foo' || c 结果为 'foo';
(3) 第一个 ? 中 'foo'为真值;
(4)(c || b)结果为'foo'
(5)第二个 ?中, 'foo'为真值
(6) a的值为42
最后结果为42
最后: 如果运算符优先级/关联规则能够令代码更为简洁,就使用运算符优先级/关联规则,如果( )有助于提高代码可读性,就使用( );
自动分号
有时JavaScript会自动为代码补上缺失的分号,即自动分号插入(Automatic Semicolon Insertion ASI;
ASI只在换行符处起作用,而不会再代码行的中间插入分号;只有代码行末尾与韩航服之间除了空格和注释之外没有别的内容,它才会这样做