汇集权威指南, 秘密花园, 设计模式, 网络文摘以及个人总结.
either the good parts or the confused parts, Node.js(Io.js) or pure JavaScript u want 2 know ;)
JavaScript是一门饱受争议的编程语言, 其存在固然有他优秀的一面. 当然, 在历史长河中得到的多数是批评, 这一点不可否认, 从它的某些"怪癖"确实能发现在语言设计上的缺陷和考虑的不周. 不过也恰恰因此使它看起来很神秘, Geek们喜欢通过Hack它来获取成就感, 比如2 == [[[2]]]
返回true
, [][(([]+[])+![])[+!![]-(~(+!![]))]+(([]+[])+{})[+!![]]+(([]+[])+!![])[+!![]]+(([]+[])+!![])[+![]]]
等于函数sort()
. 所以, 这里不管它是Good Parts还是Bad Parts, 先仔细玩味一番.
首先这里有篇阮一峰老师的文章: JavaScript有多灵活?
说起它的灵活性, 恐怕其他语言都要甘拜下风了. 如果觉得上面这篇blog中提到的玩法属于小儿科, 那么继续往下看.
注意运算符的优先级和结合性问题!
var val = 'smtg';
console.log('Value is ' + (val === 'smtg') ? 'Something' : 'Nothing');
// => Something
// 因为+相比?有更高的结合优先级
1 + - + + + - + 1
// => 2
// 记住运算的右结合规律
// 注意加号+之间有空格, 否则会被当做左值处理
NaN
与任何值都不相等, 包括其自身.- 如果两个字符串所显示的字符一样, 但具有不同编码的16位值, 那么他们被判为不等.
- 如果两个操作数类型相同, 那么和恒等比较一样.
- 如果类型不同, 则遵循如下规则:
- null和undefined相等
- 如果一个值为数字, 一个为字符串, 就先将字符串转换为数字再做比较.
- 如果一个值为布尔类型, 则转换为数字再进行比较.
- 如果一个值为对象, 另一个为数字/字符串, 则将对象先通过
valueOf()
再通过toString()
转化为原始值后进行比较, 但日期类型除外, 它直接使用toString()
转化为原始值. - 其他比较均不相等.
- 如果操作数为对象, 那么首先使用
valueOf()
, 如果不能转化为原始值, 那么使用toString()
的结果进行比较操作. - 如果两个原始值都是字符串, 将按照字符code比较大小.
- 如果两个原始值中至少一个不是字符串, 那么都转换为数字进行比较.
NaN
与任何值的比较都将返回false
.
value | to string | to number | to boolean | to object |
---|---|---|---|---|
undefined |
"undefined" |
NaN |
false |
new TypeError() |
null |
"null" |
0 |
false |
new TypeError() |
true |
true |
1 |
new Boolean(true) |
|
false |
false |
0 |
new Boolean(false) |
|
"" |
0 |
false |
new String('') |
|
"5" (数字字符串) |
5 |
true |
new String('5') |
|
"ran" (非数字字符串) |
NaN |
true |
new String('ran') |
|
±0 |
"0" |
false |
new Number(0) |
|
5 (非零) |
"5" |
true |
new Number(5) |
|
NaN |
NaN |
false |
new Number(NaN) |
|
±Infinity |
"±Infinity" |
true |
new Number(±Infinity) |
|
[] |
"" |
0 |
false |
|
[5] (一个数字元素) |
"5" |
5 |
true |
|
['ran'] (其他数组) |
join(',') |
NaN |
true |
|
function () {} |
函数源码字符串 | NaN |
true |
|
{} (任意对象) |
如下节所示 | 如下节所示 | true |
任何对象{}都有两个原始的方法toString()和valueOf(),继承自原型链的顶端——Object.prototype对象。当对象做隐式转换时,首先会自动判断将要转化为什么类型,然后按规则调用上述两个方法。规则如下:
- 如果对象要被转化为布尔型,最简单:所有引用类型或者说对象都转换成true。
- 如果转化为字符串:若有toString()方法,则调用,当该方法返回原始值时被转化为字符串,最终返回这个字符串。若没有这个方法或者该方法返回的并不是原始值,则调用valueOf()方法,如果它返回原始值则被转化为字符串并最终返回该字符串,否则转化出错,抛出TypeError。
- 如果转化为数字:和上面的情况相似,只不过先检查valueOf()后检查toString(),然后将原始值转化为数字而已,否则抛出TypeError。
var a = [1, 2, 3],
b = [1, 2, 3],
c = [1, 2, 4]
a == b // false
a === b // false
a > c // false
a < c // true
// 解释是: 在大小比较上, 对操作数调用了toString()方法, 而字符串间是通过CharCode比较大小的.
function showCase(value) {
switch(value) {
case 'A':
console.log('Case A');
break;
case undefined:
console.log('undefined');
break;
default:
console.log('Do not know!');
}
}
showCase(new String('A'));
// => Do not know!
// 解释: switch 内部使用了全等运算 ===
再看下一个:
function showCase2(value) {
switch(value) {
case 'A':
console.log('Case A');
break;
case 'B':
console.log('Case B');
break;
case undefined:
console.log('undefined');
break;
default:
console.log('Do not know!');
}
}
showCase2(String('A'));
// => case A
// 不带new的String为类型转换函数, 并不创建对象.
最后通过这个B格甚高的项目jsfuck感受一下JavaScript中匪夷所思的类型转换吧.
这里有一篇关于原型链的解释, 概述了JavaScript的继承模式.
// 1
cobj = Object.create(pobj);
// 2
cobj.__proto__ = pobj;
// 3
var Construct = function () { this.id = 'R' };
Construct.prototype = { pro: 'fun' };
Construct.call(cobj);
// 4
var Con2 = function () { this.id = 'S' };
Con2.prototype = new Construct
Con2.prototype = Construct.prototype
Con2.prototype.__proto__ = Construct.prototype
来自我的blog: 闭包, 作用域链, 内存泄露
eval之所以不被建议使用,是因为他会‘破坏作用域’。
除了eval()
,Function()
或new Function()
都能达到解析并执行代码的目的。只不过Function()
和new Function()
解析的代码只能在局部作用域执行,因此代码中的var声明的变量不会成为全局变量。而eval()却可以访问、修改它的外部作用域的变量,可能影响到全局变量。
eval如何破坏作用域?
var foo = 1;
function test() {
var foo = 2;
var bar = eval; //这里将bar设为eval的引用
bar(' foo = 3; ');
return foo;
}
test(); //返回2
console.log(foo); //返回3
正常思路来讲,test会返回foo=3,控制台打印的值为1才对吧,这就是eval破会作用域的结果。为什么eval会这么做? eval只有在被直接调用并且调用函数就是eval 本身时,解析的代码才会在当前作用域执行,否则就会像上面那样。
-
全局环境下,this指向全局对象。
-
函数直接调用时,内部this指向全局对象。
-
调用对象的方法时,函数内部this指向调用该方法的对象。
-
以构造函数的方式调用(即在函数名前加new),this指向新创建的对象.
-
显式设置this,例如:使用Function.prototype上的call来显式设置调用该方法的对象时,函数内this指向该对象。
-
直接调用在函数内部定义的闭包时,闭包内this指向全局对象。(同情况2)
var bar.getAge = function () { function getBirth() { return this.birthday; } getBirth(); //this指向全局 }; //注意:闭包中的this不会因为getBirth对象没有birthday属性而按照变量解析顺序去向上一作用域寻找birthday,因为this是关键字,而非变量! //要想获取bar对象,应该在getAge内部声明一个变量来存储this,然后将这个变量以参数形式传入getBirth: var bar.getAge = function () { var that = this; function getBirth(that) { return that.birthday; } getBirth(that); //正常返回 };
-
将方法赋值给一个变量,则函数内部this指向全局对象(此时相当于情况2)。
var tes = bar.getAge; bar.getAge();//this指向bar tes();//this指向全局对象
-
将函数/方法以参数形式传给另一个函数时,参数this指向全局对象,这有点像“将函数/方法赋值给形参”(同情况7)。
function foo(callback) { callback(); } foo(bar.done);//done的this不会再指向bar,而是全局对象
-
对象字面量声明中,this不能用来指向对象本身,而是当前有效作用域的全局对象:
var obj = { name: 'mmm', do: this.geName, //这里就不能这么写了,因为this为全局对象,正确的写法是像下面的方法这样以获取obj对象 getName: function { return this.name; } };
var reverse = [].reverse;
reverse();
// => window or TypeError
// 因为reverse返回this
当函数以new调用,则会表现的与普通调用有所不同。表面上看就是产生了一个标准对象,其实这个调用过程中函数内部是这样的:
- 首先创建一个空对象,该对象自动继承了构造函数的原型。
- 构造函数中定义的属性/方法也被添加到刚刚创建的对象里。
- 新创建的对象被构造函数内的this引用。
- 最后,如果函数没有显示设置return的对象,则 隐式返回这个新创建的对象。
** 注意构造函数的有效return返回的永远是一个对象,而不可能是其他类型 所以,类似return 2、return 'hey'的返回值都不会被真正返回,这时仍然返回内部创建的对象.**
比如当访问函数内变量“ran”时,会按如下顺序寻找ran:
- 当前作用域是否有“ran”?是,则访问成功;否,则跳到2.
- 函数形参是否使用“ran”为名?是,则访问成功;否,则跳到3.
- 函数自身的标识(函数名)是否叫“ran”?是,则访问成功;否,则跳到4.
- 回溯到上一层作用域,并同时跳到1.,如此循环直到当前作用域为顶层作用域为止,若仍未找到“ran”,则返回undefined。
var f = function fn() {};
f();
fn();
// => ReferenceError
(function func() {})();
func();
// => ReferenceError
// 两处ReferenceError, 是因为"立即函数"调用相当于立刻调用函数表达式, 所以函数的name并不在当前作用域中.
function sidEffecting(ary) {
ary[0] = ary[2];
}
function bar(a,b,c) {
c = 10
sidEffecting(arguments);
return a + b + c;
}
bar(1,1,1);
// => 21
// 记得arguments与函数形参之间有一个双向绑定(互相映射), 就算他们不在同一个作用域.
// 上面这个例子很好的说明了这点.
// notice: ES5移除了这一特性。
// 并且如果使用了"use stricts"语句(严格模式),arguments会变成保留字,也就是说不能将arguments当做变量名使用,不能给其赋值。
函数内部无论在那里声明变量,效果都等同于在函数顶部进行声明。
[].reduce(function () {})
// throw TypeError
// 对一个空数组进行无始值reduce调用会抛出TypeError
var ary = [0,1,2];
ary[10] = 10;
ary.filter(function(x) { return x === undefined;});
// => []
// 因为filter只作用在非空元素上. (除了filter还有map也是这样)
var arr = new Array(3);
var ary = [,,,];
// 两个创建方式是等效的, 都会产生一个稀疏数组, 并且元素尚未存在. 和下面是不一样的:
var ar = [undefined, undefined, undefined];
0 in ary;
// => false
0 in ar;
// => true
Infinity % ANY_NUMBER
// => NaN
var min = Math.min(), max = Math.max()
min < max;
// => false
// 如果不传入参数, min()返回Infinity, max()返回-Infinity
Number.MIN_VALUE > 0
// => true
// MIN_VALUE是一个大于0的最小值:)
这段代码不会打印101, 而是进入一个死循环. 就算在在JavaScript世界里也很令人费解, 究竟发生了什么?
var END = Math.pow(2, 53);
var START = END - 100;
var count = 0;
for (var i = START; i <= END; i++) {
count++;
}
console.log(count);
这还有个蛋疼的例子
var a = 111111111111111110000,
b = 1111;
a + b;
// => 111111111111111110000
别急, 还有更奇葩的数学计算
var two = 0.2
var one = 0.1
var eight = 0.8
var six = 0.6
[two - one == one, eight - six == two]
// => [true, false]
// 实际上eight - six == 0.20000000000000007
这是因为JavaScript中的浮点计算导致的. 了解一下背景知识:
JavaScript采用IEEE 754标准定义的64位浮点格式表示数字. 这是一种二进制表示法, 可以精确表示如
1/2
,1/8
,1/1024
等分数, 但十进制分数如1/10
,1/100
等则不能精确表示. JavaScript中能表示的整数范围是[2^-53, 2^53]
, 如果使用超出此范围的整数, 将无法保证低位数字的精度! 然而实际的操作(如数组索引,位操作符)则是基于32位整数的.
"1 2 3".replace(/\d/g, parseInt);
// => 1 NaN 3
要理解这个结果首先要知道replace
的用法. 当replace
第二个参数为回调时, 会传递三个参数: 第一个匹配项, 匹配项索引, 母串. 而parseInt(2, 2)是非法运算, 返回为NaN.
function captureOne(re, str) {
var match = re.exec(str);
return match && match[1];
}
var numRe = /num=(\d+)/ig,
wordRe = /word=(\w+)/i,
a1 = captureOne(numRe, "num=1"),
a2 = captureOne(wordRe, "word=1"),
a3 = captureOne(numRe, "NUM=2"),
a4 = captureOne(wordRe, "WORD=2");
[a1 === a2, a3 === a4];
// => [true, false]
这是由于正则表达式中g
引起的. g
代表全局匹配, 启用后正则对象会记录lastIndex, 下次从这个索引继续匹配(不管是否为同一个字符串). 一旦匹配成功, 就会更新lastIndex, 否则lastIndex为0.
var a = new Date("epoch");
// => Invaild Date
// 不会抛出错误. o_O
var a = Date(0);
var b = new Date(0);
var c = new Date();
[a === b, b === c, a === c];
// => [false, false, false]
// 以普通函数调用Date()只会返回一个字符串
var a = new Date("2014-03-19"),
b = new Date(2014, 03, 19);
[a.getDay() === b.getDay(), a.getMonth() === b.getMonth()]
// => [false, false]
Date构造函数可以传入多种类型的参数, 但如果是传入多个参数, 代表月份的第二个参数是从0开始的, 也就是3代表4月. 详见MDN对Date的解释.