前端知识总结
对象
对象使用和属性
JavaScript 中所有变量都可以当作对象使用,除了两个例外 null 和 undefined。
false.toString(); // 'false'
[1, 2, 3].toString(); // '1,2,3'
function Foo() {}
Foo.bar = 1;
Foo.bar; // 1
点操作符解析为浮点数字面值的一部分。
2.toString(); // 出错:SyntaxError
有很多变通方法可以让数字的字面值看起来像对象。
(2).toString(); // 第二个点号可以正常解析
(2).toString(); // 注意点号前面的空格
(2).toString(); // 2先被计算
对象作为数据类型
JavaScript 的对象可以作为 哈希表使用,主要用来保存命名的键与值的对应关系。
{}
- 可以创建一个简单对象。这个新创建的对象从Object.prototype
继承下面,没有任何 自定义属性。
var foo = {}; // 一个空对象
// 一个新对象,拥有一个值为12的自定义属性'test'
var bar = {test: 12};
111
#### 访问属性
有两种方式来访问对象的属性,点操作符或者中括号操作符。
```js
var foo = {name: 'kitten'}
foo.name; // kitten
foo['name']; // kitten
var get = 'name';
foo[get]; // kitten
foo.1234; // SyntaxError
foo['1234']; // works
两种语法是等价的,但是中括号操作符在下面两种情况下依然有效
- 动态设置属性
- 译者注:比如属性名中包含空格,或者属性名是 JS 的关键词)
译者注:在 JSLint 语法检测工具中,点操作符是推荐做法。
删除属性
delete
操作符;设置属性为 undefined
或者null
并不能真正的删除属性, 而仅仅是移除了属性和值的关联。
var obj = {
bar: 1,
foo: 2,
baz: 3,
};
obj.bar = undefined;
obj.foo = null;
delete obj.baz;
for (var i in obj) {
if (obj.hasOwnProperty(i)) {
console.log(i, "" + obj[i]);
}
}
bar undefined
和 foo null
- 只有 baz
属性名的语法
var test = {
case: "I am a keyword so I must be notated as a string",
delete: "I am a keyword too so me", // 出错:SyntaxError
};
SyntaxError``delete
是 JavaScript 语言的一个关键词;因此为了在更低版本的 JavaScript 引擎下也能正常运行, 必须使用字符串字面值声明方式。
原型
prototype
虽然这经常被当作是 JavaScript 的缺点被提及,其实基于原型的继承模型比传统的类继承还要强大。 实现传统的类继承模型是很简单,但是实现 JavaScript 中的原型继承则要困难的多。 (It is for example fairly trivial to build a classic model on top of it, while the other way around is a far more difficult task.)
由于 JavaScript 是唯一一个被广泛使用的基于原型继承的语言,所以理解两种继承模式的差异是需要一定时间的。
原型链的继承方式。
注意: 简单的使用Bar.prototype = Foo.prototype
将会导致两个对象共享相同的原型。 因此,改变任意一个对象的原型都会影响到另一个对象的原型,在大多数情况下这不是希望的结果。
function Foo() {
this.value = 42;
}
Foo.prototype = {
method: function () {},
};
function Bar() {}
// 设置Bar的prototype属性为Foo的实例对象
Bar.prototype = new Foo();
Bar.prototype.foo = "Hello World";
// 修正Bar.prototype.constructor为Bar本身
Bar.prototype.constructor = Bar;
var test = new Bar(); // 创建Bar的一个新实例
// 原型链
test [Bar的实例]
Bar.prototype [Foo的实例]
{ foo: 'Hello World' }
Foo.prototype
{method: ...};
Object.prototype
{toString: ... /* etc. */};
test
对象从 Bar.prototype
和 Foo.prototype
继承下来;因此, 它能访问 Foo
的原型方法 method
。同时,它也能够访问那个定义在原型上的 Foo
实例属性 value
。 需要注意的是 new Bar()
不会创造出一个新的 Foo
实例,而是 重复使用它原型上的那个实例;因此,所有的 Bar
实例都会共享相同的 value
注意: 不要使用Bar.prototype = Foo
,因为这不会执行 Foo
的原型,而是指向函数 Foo
。 因此原型链将会回溯到Function.prototype
而不是Foo.prototype
,因此method
属性查找
向上遍历原型链,直到找到给定名称的属性为止。
Object.prototype
- 但是仍然没有找到指定的属性,就会返回 undefined。
原型属性
任何类型的值赋给它(prototype)。 然而将原子类型赋给 prototype 的操作将会被忽略。
function Foo() {}
Foo.prototype = 1; // 无效
而将对象赋值给 prototype,正如上面的例子所示,将会动态的创建原型链。
性能
如果一个属性在原型链的上端,则对于查找时间将带来不利影响。特别的,试图获取一个不存在的属性将会遍历整个原型链。
并且,当使用 [for in](http://bonsaiden.github.io/JavaScript-Garden/zh/#object.forinloop)
循环遍历对象的属性时,原型链上的所有属性都将被访问。
扩展内置类型的原型
Object.prototype
这种技术被称之为 monkey patching 并且会破坏封装。虽然它被广泛的应用到一些 JavaScript 类库中比如 Prototype, 但是我仍然不认为为内置类型添加一些非标准的函数是个好主意。
唯一理由是为了和新的 JavaScript 保持一致,比如 [Array.forEach](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/forEach)
。
译者注:这是编程领域常用的一种方式,称之为 Backport,也就是将新的补丁添加到老版本中。
总结
必修的功课。 要提防原型链过长带来的性能问题,并知道如何通过缩短原型链来提高性能。 更进一步,绝对不要扩展内置类型的原型,除非是为了和新的 JavaScript 引擎兼容。
hasOwnProperty
自定义属性而不是 原型链上的属性, 我们需要使用继承自 Object.prototype
的 hasOwnProperty
注意: 通过判断一个属性是否undefined
是不够的。 因为一个属性可能确实存在,只不过它的值被设置为undefined
。hasOwnProperty
是 JavaScript 中唯一一个处理属性但是不查找原型链的函数。
// 修改Object.prototype
Object.prototype.bar = 1;
var foo = { goo: undefined };
foo.bar; // 1
"bar" in foo; // true
foo.hasOwnProperty("bar"); // false
foo.hasOwnProperty("goo"); // true
hasOwnProperty
可以给出正确和期望的结果,这在遍历对象的属性时会很有用。 没有其它方法可以用来排除原型链上的属性,而不是定义在对象自身上的属性。
hasOwnProperty
不会保护 hasOwnProperty
被非法占用,因此如果一个对象碰巧存在这个属性, 就需要使用外部的 hasOwnProperty
var foo = {
hasOwnProperty: function () {
return false;
},
bar: "Here be dragons",
};
foo.hasOwnProperty("bar"); // 总是返回 false
// 使用其它对象的 hasOwnProperty,并将其上下文设置为foo
({}).hasOwnProperty.call(foo, "bar"); // true
结论
hasOwnProperty
是唯一可用的方法。 同时在使用 [for in](http://bonsaiden.github.io/JavaScript-Garden/zh/#object.forinloop)
遍历对象时,推荐总是使用 hasOwnProperty
方法, 这将会避免 原型对象扩展带来的干扰。
for in
in
操作符一样,for in
注意: for in
循环不会遍历那些 enumerable
设置为false
的属性;比如数组的length
// 修改 Object.prototype
Object.prototype.bar = 1;
var foo = { moo: 2 };
for (var i in foo) {
console.log(i); // 输出两个属性:bar 和 moo
}
for in
自身的行为,因此有必要过滤出那些不希望出现在循环体中的属性, 这可以通过 Object.prototype
原型上的 [hasOwnProperty](http://bonsaiden.github.io/JavaScript-Garden/zh/#object.hasownproperty)
函数来完成。注意: 由于 for in
hasOwnProperty
// foo 变量是上例中的
for (var i in foo) {
if (foo.hasOwnProperty(i)) {
console.log(i);
}
}
hasOwnProperty
,所以这次只输出 moo
。 如果不使用 hasOwnProperty
,则这段代码在原生对象原型(比如 Object.prototype
)被扩展时可能会出错。 一个广泛使用的类库 Prototype 就扩展了原生的 JavaScript 对象。 因此,当这个类库被包含在页面中时,不使用 hasOwnProperty
过滤的 for in
总结
总是使用 hasOwnProperty
。不要对代码运行的环境做任何假设,不要假设原生对象是否已经被扩展了。
函数
函数声明与表达式
匿名函数作为回调函数传递到异步函数中。
函数声明
function foo() {}
上面的方法会在执行前被 解析(hoisted),因此它存在于当前上下文的任意一个地方, 即使在函数定义体的上面被调用也是对的。
foo(); // 正常运行,因为foo在代码运行前已经被创建
function foo() {}
函数赋值表达式
var foo = function () {};
匿名的函数赋值给变量 foo
。
foo; // 'undefined'
foo(); // 出错:TypeError
var foo = function () {};
var
定义了一个声明语句,对变量 foo
的解析是在代码运行之前,因此foo``foo
的值缺省为 undefined。
命名函数的赋值表达式
另外一个特殊的情况是将命名函数赋值给一个变量。
var foo = function bar() {
bar(); // 正常运行
};
bar(); // 出错:ReferenceError
bar
函数声明外是不可见的,这是因为我们已经把函数赋值给了 foo
; 然而在 bar
内部依然可见。这是由于 JavaScript 的 命名处理 所致, 函数名在函数内总是可见的。注意:在 IE8 及 IE8 以下版本浏览器 bar 在外部也是可见的,是因为浏览器对命名函数赋值表达式进行了错误的解析, 解析成两个函数 foo
和 bar
this
this
的处理机制。 在五种不同的情况下 ,this
全局范围内
this;
this
,它将会指向全局对象。译者注:浏览器中运行的 JavaScript 脚本,这个全局对象是 window
。
函数调用
foo();
this
也会指向全局对象。ES5 注意: 在严格模式下(strict mode),不存在全局变量。 这种情况下 this
将会是 undefined
。
方法调用
test.foo();
this
指向 test
调用构造函数
new foo();
new
关键词一块使用,则我们称这个函数是 构造函数。 在函数内部,this
指向新创建的对象。
this
function foo(a, b, c) {}
var bar = {};
foo.apply(bar, [1, 2, 3]); // 数组将会被扩展,如下所示
foo.call(bar, 1, 2, 3); // 传递到foo的参数是:a = 1, b = 2, c = 3
Function.prototype
上的 call
或者 apply
方法时,函数内的 this
将会被 显式设置为函数调用的第一个参数。函数调用的规则在上例中已经不适用了,在foo
函数内 this
被设置成了bar
。注意: 在对象的字面声明语法中,this
不能用来指向对象本身。 因此 var obj = {me: this}
中的 me
不会指向obj
,因为 this
只可能出现在上述的五种情况中。 译者注:这个例子中,如果是在浏览器中运行,obj.me
等于window
常见误解
译者注:这里指的应该是第二个规则,也就是直接调用函数时,this
指向全局对象) 被认为是 JavaScript 语言另一个错误设计的地方,因为它从来就没有实际的用途。
Foo.method = function () {
function test() {
// this 将会被设置为全局对象(译者注:浏览器环境中也就是 window 对象)
}
test();
};
test
中的 this
将会指向 Foo
对象,实际上不是这样子的。test
中获取对 Foo
对象的引用,我们需要在 method
函数内部创建一个局部变量指向 Foo
Foo.method = function () {
var that = this;
function test() {
// 使用 that 来指向 Foo 对象
}
test();
};
that
只是我们随意起的名字,不过这个名字被广泛的用来指向外部的 this
对象。 在 闭包 一节,我们可以看到 that
方法的赋值表达式
赋值给一个变量。
var test = someObject.methodTest;
test();
test
就像一个普通的函数被调用;因此,函数内的 this
将不再被指向到 someObject``this
的晚绑定特性似乎并不友好,但这确实是 基于原型继承赖以生存的土壤。
function Foo() {}
Foo.prototype.method = function () {};
function Bar() {}
Bar.prototype = Foo.prototype;
new Bar().method();
method
被调用时,this
将会指向 Bar
闭包和引用
总是能够访问外部作用域中的变量。 因为 函数 是 JavaScript 中唯一拥有自身作用域的结构,因此闭包的创建依赖于函数。
模拟私有变量
function Counter(start) {
var count = start;
return {
increment: function () {
count++;
},
get: function () {
return count;
},
};
}
var foo = Counter(4);
foo.increment();
foo.get(); // 5
Counter
函数返回两个闭包,函数 increment
和函数 get
。 这两个函数都维持着 对外部作用域 Counter
的引用,因此总可以访问此作用域内定义的变量 count
.
为什么不可以在外部访问私有变量
count
var foo = new Counter(4);
foo.hack = function () {
count = 1337;
};
不会改变定义在 Counter
作用域中的 count
变量的值,因为foo.hack
没有 定义在那个作用域内。它将会创建或者覆盖全局变量count
。
循环中的闭包
一个常见的错误出现在循环中使用闭包,假设我们需要在每次循环中调用循环序号
for (var i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
0
到 9
,而是会输出数字 10``console.log
被调用的时候,匿名函数保持对外部变量 i
的引用,此时for
循环已经结束, i
的值被修改成了 10
.i
的拷贝。
避免引用错误
为了正确的获得循环序号,最好使用 匿名包装器(译者注:其实就是我们通常说的自执行匿名函数)。
for (var i = 0; i < 10; i++) {
(function (e) {
setTimeout(function () {
console.log(e);
}, 1000);
})(i);
}
i
作为它的参数,此时函数内 e
变量就拥有了 i``setTimeout
的匿名函数执行时,它就拥有了对 e
的引用,而这个值是不会被循环改变的。
有另一个方法完成同样的工作,那就是从匿名包装器中返回一个函数。这和上面的代码效果一样。
for (var i = 0; i < 10; i++) {
setTimeout(
(function (e) {
return function () {
console.log(e);
};
})(i),
1000
);
}
arguments
arguments
。这个变量维护着所有传递到这个函数中的参数列表。注意: 由于 arguments
已经被定义为函数内的一个变量。 因此通过 var
关键字定义arguments
或者将arguments
声明为一个形式参数, 都将导致原生的arguments``arguments
变量不是一个数组(Array
)。 尽管在语法上它有数组相关的属性 length
,但它不从 Array.prototype
继承,实际上它是一个对象(Object
)。arguments
变量使用标准的数组方法,比如 push
, pop
或者slice
。 虽然使用 for
转化为数组
arguments
Array.prototype.slice.call(arguments);
慢,在性能不好的代码中不推荐这种做法。
传递参数
下面是将参数从一个函数传递到另一个函数的推荐做法。
function foo() {
bar.apply(null, arguments);
}
function bar(a, b, c) {
// 干活
}
call
和 apply
,创建一个快速的解绑定包装器。
function Foo() {}
Foo.prototype.method = function (a, b, c) {
console.log(this, a, b, c);
};
// 创建一个解绑定的 "method"
// 输入参数为: this, arg1, arg2...argN
Foo.method = function () {
// 结果: Foo.prototype.method.call(this, arg1, arg2... argN)
Function.call.apply(Foo.prototype.method, arguments);
};
注:上面的 Foo.method
Foo.method = function () {
var args = Array.prototype.slice.call(arguments);
Foo.prototype.method.apply(args[0], args.slice(1));
};
自动更新
arguments
对象为其内部属性以及函数形式参数创建 getter 和 setterarguments
function foo(a, b, c) {
arguments[0] = 2;
a; // 2
b = 4;
arguments[1]; // 4
var d = c;
d = 9;
c; // 3
}
foo(1, 2, 3);
性能真相
arguments``arguments
的 getters 和 setters 方法总会被创建;因此使用 arguments
对性能不会有什么影响。 除非是需要对 arguments
ES5 提示: 这些 getters 和 setters
译者注:在 MDC 中对 strict mode
模式下 arguments
// 阐述在 ES5 的严格模式下 `arguments` 的特性
function f(a) {
"use strict";
a = 42;
return [a, arguments[0]];
}
var pair = f(17);
console.assert(pair[0] === 42);
console.assert(pair[1] === 17);
arguments.callee
。
function foo() {
arguments.callee; // do something with this function object
arguments.callee.caller; // and the calling function object
}
function bigLoop() {
for (var i = 0; i < 100000; i++) {
foo(); // Would normally be inlined...
}
}
foo
不再是一个单纯的内联函数 inlining(译者注:这里指的是解析器可以做内联处理), 因为它需要知道它自己和它的调用者。 这不仅抵消了内联函数带来的性能提升,而且破坏了封装,因此现在函数可能要依赖于特定的上下文。强烈建议大家不要使用 arguments.callee
ES5 提示: 在严格模式下,arguments.callee
会报错 TypeError
,因为它已经被废除了。
构造函数
new``this
指向新创建的对象 Object
。 这个新创建的对象的 [prototype](http://bonsaiden.github.io/JavaScript-Garden/zh/#object.prototype)
被指向到构造函数的 prototype
。return
表达式,则隐式的会返回 this
function Foo() {
this.bla = 1;
}
Foo.prototype.test = function () {
console.log(this.bla);
};
var test = new Foo();
Foo
作为构造函数调用,并设置新创建对象的 prototype
为Foo.prototype
。return
表达式将会影响返回结果,但仅限于返回的是一个对象。
function Bar() {
return 2;
}
new Bar(); // 返回新创建的对象
function Test() {
this.value = 2;
return {
foo: 1,
};
}
new Test(); // 返回的对象
译者注:new Bar()
返回的是新创建的对象,而不是数字的字面值 2。 因此new Bar().constructor === Bar
,但是如果返回的是数字对象,结果就不同了,如下所示
function Bar() {
return new Number(2);
}
new Bar().constructor === Number;
译者注:这里得到的 new Test()
是函数返回的对象,而不是通过new
关键字新创建的对象,因此:
(new Test().value === undefined(new Test()).foo) === 1;
new
被遗漏了,则函数不会返回新创建的对象。
function Foo() {
this.bla = 1; // 获取设置全局参数
}
Foo(); // undefined
虽然上例在有些情况下也能正常运行,但是由于 JavaScript 中 [this](http://bonsaiden.github.io/JavaScript-Garden/zh/#function.this)
的工作原理, 这里的 this
指向全局对象。
工厂模式
new
function Bar() {
var value = 1;
return {
method: function () {
return value;
},
};
}
Bar.prototype = {
foo: function () {},
};
new Bar();
Bar();
Bar
函数的调用返回的值完全相同,一个新创建的拥有 method
属性的对象被返回, 其实这里创建了一个 闭包。new Bar()
并不会改变返回对象的原型(译者注:也就是返回对象的原型不会指向 Bar.prototype
)。 因为构造函数的原型会被指向到刚刚创建的新对象,而这里的 Bar
没有把这个新对象返回(译者注:而是返回了一个包含 method``new
译者注:上面两种方式创建的对象不能访问 Bar
var bar1 = new Bar();
typeof bar1.method; // "function"
typeof bar1.foo; // "undefined"
var bar2 = Bar();
typeof bar2.method; // "function"
typeof bar2.foo; // "undefined"
通过工厂模式创建新对象
不要使用 new
为了创建新对象,我们可以创建一个工厂方法,并且在方法内构造一个新对象。
function Foo() {
var obj = {};
obj.value = "blub";
var private = 2;
obj.someMethod = function (value) {
this.value = value;
};
obj.getPrivate = function () {
return private;
};
return obj;
}
new
的调用方式不容易出错,并且可以充分利用 私有变量带来的便利, 但是随之而来的是一些不好的地方。
- 不能共享原型上的方法。
- 为了实现继承,工厂方法需要从另外一个对象拷贝所有属性,或者把一个对象作为新创建对象的原型。
new
总结
new
关键字可能会导致问题,但这并不是放弃使用原型链的借口。 最终使用哪种方式取决于应用程序的需求,选择一种代码书写风格并坚持下去才是最重要的。
作用域与命名空间
函数作用域。
function test() {
// 一个作用域
for (var i = 0; i < 10; i++) {
// 不是一个作用域
// count
}
console.log(i); // 10
}
注意: 如果不是在赋值语句中,而是在 return 表达式或者函数参数中,{...}
将会作为代码段解析, 而不是作为对象的字面语法解析。如果考虑到 自动分号插入,这可能会导致一些不易察觉的错误。译者注:如果 return
对象的左括号和 return
// 译者注:下面输出 undefined
function add(a, b) {
return;
a + b;
}
console.log(add(1, 2));
全局共享的命名空间下面。
ReferenceError
异常。
隐式的全局变量
// 脚本 A
foo = "42";
// 脚本 B
var foo = "42";
不同。脚本 A 在全局作用域内定义了变量 foo
,而脚本 B 在当前作用域内定义变量 foo
。完全不同,不使用 var
// 全局作用域
var foo = 42;
function test() {
// 局部作用域
foo = 21;
}
test();
foo; // 21
test
内不使用 var
关键字声明 foo
变量将会覆盖外部的同名变量。 起初这看起来并不是大问题,但是当有成千上万行代码时,不使用 var
// 全局作用域
var items = [
/* 数组 */
];
for (var i = 0; i < 10; i++) {
subLoop();
}
function subLoop() {
// subLoop 函数作用域
for (i = 0; i < 10; i++) {
// 没有使用 var 声明变量
// 干活
}
}
subLoop
之后就会终止,因为 subLoop
覆盖了全局变量 i
。 在第二个 for
循环中使用 var
声明变量可以避免这种错误。 声明变量时绝对不要遗漏 var
关键字,除非这就是期望的影响外部作用域的行为。
局部变量
JavaScript 中局部变量只可能通过两种方式声明,一个是作为 函数参数,另一个是通过 var
// 全局变量
var foo = 1;
var bar = 2;
var i = 2;
function test(i) {
// 函数 test 内的局部作用域
i = 5;
var foo = 3;
bar = 4;
}
test(10);
foo
和 i
是函数 test
内的局部变量,而对 bar
变量声明提升(Hoisting)
提升变量声明。这意味着 var
表达式和 function
bar();
var bar = function () {};
var someValue = 42;
test();
function test(data) {
if (false) {
goo = 1;
} else {
var goo = 2;
}
for (var i = 0; i < 100; i++) {
var e = data[i];
}
}
var
表达式和 function
声明提升到当前作用域的顶部。
// var 表达式被移动到这里
var bar, someValue; // 缺省值是 'undefined'
// 函数声明也会提升
function test(data) {
var goo, i, e; // 没有块级作用域,这些变量被移动到函数顶部
if (false) {
goo = 1;
} else {
goo = 2;
}
for (i = 0; i < 100; i++) {
e = data[i];
}
}
bar(); // 出错:TypeError,因为 bar 依然是 'undefined'
someValue = 42; // 赋值语句不会被提升规则(hoisting)影响
bar = function () {};
test();
var
表达式被从循环内移到外部,而且使一些 if
表达式更难看懂。if
表达式看起来修改了全局变量 goo
,实际上在提升规则被应用后,却是在修改局部变量。ReferenceError
。
// 检查 SomeImportantThing 是否已经被初始化
if (!SomeImportantThing) {
var SomeImportantThing = {};
}
var
表达式会被提升到全局作用域的顶部。
var SomeImportantThing;
// 其它一些代码,可能会初始化 SomeImportantThing,也可能不会
// 检查是否已经被初始化
if (!SomeImportantThing) {
SomeImportantThing = {};
}
译者注:在 Nettuts+ 网站有一篇介绍 hoisting 的 文章,其中的代码很有启发性。
// 译者注:来自 Nettuts+ 的一段代码,生动的阐述了 JavaScript 中变量声明提升规则
var myvar = "my value";
(function () {
alert(myvar); // undefined
var myvar = "local value";
})();
名称解析顺序
全局作用域,都有一个特别的名称 [this](http://bonsaiden.github.io/JavaScript-Garden/zh/#function.this)
指向当前对象。 函数作用域内也有默认的变量 [arguments](http://bonsaiden.github.io/JavaScript-Garden/zh/#function.arguments)
,其中包含了传递到函数中的参数。foo``var foo``foo``foo
注意: 自定义 arguments
参数将会阻止原生的 arguments
对象的创建。
命名空间
匿名包装器
(function () {
// 函数创建一个命名空间
window.foo = function () {
// 对外公开的函数,创建了闭包
};
})(); // 立即执行此匿名函数
匿名函数被认为是 表达式;因此为了可调用性,它们首先会被执行。
// 小括号内的函数首先被执行
(function () {})(); // 并且返回函数对象 // 调用上面的执行结果,也就是函数对象
有一些其他的调用函数表达式的方法,比如下面的两种方式语法不同,但是效果一模一样。
// 另外两种方式
+(function () {})();
(function () {})();
结论
匿名包装器(译者注:也就是自执行的匿名函数)来创建命名空间。这样不仅可以防止命名冲突, 而且有利于程序的模块化。
不好的习惯。这样的代码容易产生错误并且维护成本较高。
数组
数组遍历与属性
虽然在 JavaScript 中数组是对象,但是没有好的理由去使用 [for in](http://bonsaiden.github.io/JavaScript-Garden/zh/#object.forinloop)
遍历数组。 相反,有一些好的理由不去使用 for in
注意: JavaScript 中数组不是关联数组。 JavaScript 中只有 对象 来管理键值的对应关系。但是关联数组是保持顺序的,而对象不是。
for in
循环会枚举原型链上的所有属性,唯一过滤这些属性的方式是使用 [hasOwnProperty](http://bonsaiden.github.io/JavaScript-Garden/zh/#object.hasownproperty)
函数, 因此会比普通的 for
遍历
for
var list = [1, 2, 3, 4, 5, ...... 100000000];
for(var i = 0, l = list.length; i < l; i++) {
console.log(list[i]);
}
l = list.length``length
是数组的一个属性,但是在每次循环中访问它还是有性能开销。可能最新的 JavaScript 引擎在这点上做了优化,但是我们没法保证自己的代码是否运行在这些最近的引擎之上。
实际上,不使用缓存数组长度的方式比缓存版本要慢很多。
length
length
属性的 getter 方式会简单的返回数组的长度,而 setter 方式会截断数组。
var foo = [1, 2, 3, 4, 5, 6];
foo.length = 3;
foo; // [1, 2, 3]
foo.length = 6;
foo; // [1, 2, 3]
译者注: 在 Firebug 中查看此时 foo
的值是: [1, 2, 3, undefined, undefined, undefined]
但是这个结果并不准确,如果你在 Chrome 的控制台查看 foo
的结果,你会发现是这样的: [1, 2, 3]
因为在 JavaScript 中undefined
// 译者注:为了验证,我们来执行下面代码,看序号 5 是否存在于 foo 中。
5 in foo; // 不管在 Firebug 或者 Chrome 都返回 false
foo[5] = undefined;
5 in foo; // 不管在 Firebug 或者 Chrome 都返回 true
length
设置一个更小的值会截断数组,但是增大 length
结论
for
循环并缓存数组的 length
属性。 使用 for in
Array
Array
的构造函数在如何处理参数时有点模棱两可,因此总是推荐使用数组的字面语法 - []
[1, 2, 3]; // 结果: [1, 2, 3]
new Array(1, 2, 3); // 结果: [1, 2, 3]
[3]; // 结果: [3]
new Array(3); // 结果: []
new Array("3"); // 结果: ['3']
// 译者注:因此下面的代码将会使人很迷惑
new Array(3, 4, 5); // 结果: [3, 4, 5]
new Array(3); // 结果: [],此数组长度为 3
译者注:这里的模棱两可指的是数组的 两种构造函数语法
new Array(3);
这种调用方式),并且这个参数是数字,构造函数会返回一个 length
属性被设置为此参数的空数组。 需要特别注意的是,此时只有 length
译者注:在 Firebug 中,你会看到 [undefined, undefined, undefined]
,这其实是不对的。在上一节有详细的分析。
var arr = new Array(3);
arr[1]; // undefined
1 in arr; // false, 数组还没有生成
for
new Array(count + 1).join(stringToRepeat);
译者注: new Array(3).join('#')
将会返回 ##
结论
应该尽量避免使用数组构造函数创建新数组。推荐使用数组的字面语法。它们更加短小和简洁,因此增加了代码的可读性。
类型
相等与比较
JavaScript 有两种方式判断两个值是否相等。
等于操作符
==
弱类型语言,这就意味着,等于操作符会为了比较两个值而进行强制类型转换。
"" == "0"; // false
0 == ""; // true
0 == "0"; // true
false == "false"; // false
false == "0"; // true
false == undefined; // false
false == null; // false
null == undefined; // true
" \t\r\n" == 0; // true
==
此外,强制类型转换也会带来性能消耗,比如一个字符串为了和一个数字进行比较,必须事先被强制转换为数字。
严格等于操作符
三个等号组成:===
不会进行强制类型转换。
"" === "0"; // false
0 === ""; // false
0 === "0"; // false
false === "false"; // false
false === "0"; // false
false === undefined; // false
false === null; // false
null === undefined; // false
" \t\r\n" === 0; // false
上面的结果更加清晰并有利于代码的分析。如果两个操作数类型不同就肯定不相等也有助于性能的提升。
比较对象
==
和 ===
{} === {}; // false
new String('foo') === 'foo'; // false
new Number(10) === 10; // false
var foo = {};
foo === foo; // true
不是值是否相等,而是是否属于同一个身份;也就是说,只有对象的同一个实例才被认为是相等的。 这有点像 Python 中的 is
注意:为了更直观的看到==
和===
的区别,可以参见 JavaScript Equality Table
结论
严格等于操作符。如果类型需要转换,应该在比较之前 显式的转换, 而不是使用语言本身复杂的强制转换规则。
typeof
typeof
操作符(和 [instanceof](http://bonsaiden.github.io/JavaScript-Garden/zh/#types.instanceof)
一起)或许是 JavaScript 中最大的设计缺陷, 因为几乎不可能从它们那里得到想要的结果。instanceof
还有一些极少数的应用场景,typeof
只有一个实际的应用(译者注:这个实际应用是用来检测一个对象是否已经定义或者是否已经赋值), 而这个应用却不是用来检查对象的类型。注意: 由于 typeof
也可以像函数的语法被调用,比如typeof(obj)
,但这并不是一个函数调用。 那两个小括号只是用来计算一个表达式的值,这个返回值会作为 typeof
操作符的一个操作数。 实际上不存在名为 typeof
JavaScript 类型表格
Value Class Type
-------------------------------------
"foo" String string
new String("foo") String object
1.2 Number number
new Number(1.2) Number object
true Boolean boolean
new Boolean(true) Boolean object
new Date() Date object
new Error() Error object
[1,2,3] Array object
new Array(1, 2, 3) Array object
new Function("") Function function
/abc/g RegExp object (function in Nitro/V8)
new RegExp("meow") RegExp object (function in Nitro/V8)
{} Object object
new Object() Object object
Type 一列表示 typeof
Class 一列表示对象的内部属性 [[Class]]
JavaScript 标准文档中定义:[[Class]]
的值只可能是下面字符串中的一个:Arguments
, Array
,Boolean
, Date
, Error
,Function
, JSON
, Math
,Number
, Object
, RegExp
,String
.[[Class]]
,我们需要使用定义在 Object.prototype
上的方法 toString
。
对象的类定义
[[Class]]
值的方法,那就是使用Object.prototype.toString
。
function is(type, obj) {
var clas = Object.prototype.toString.call(obj).slice(8, -1);
return obj !== undefined && obj !== null && clas === type;
}
is("String", "test"); // true
is("String", new String("test")); // true
Object.prototype.toString
方法被调用, this 被设置为了需要获取 [[Class]]
译者注:Object.prototype.toString
返回一种标准格式字符串,所以上例可以通过 slice
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call(2); // "[object Number]"
ES5 提示: 在 ECMAScript 5 中,为了方便,对 null
和undefined
调用Object.prototype.toString
方法, 其返回值由 Object
变成了 Null
和 Undefined
。
译者注:这种变化可以从 IE8 和 Firefox 4 中看出区别,如下所示:
// IE8
Object.prototype.toString.call(null); // "[object Object]"
Object.prototype.toString.call(undefined); // "[object Object]"
// Firefox 4
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
测试为定义变量
typeof foo !== "undefined";
foo
是否已经定义;如果没有定义而直接使用会导致ReferenceError
的异常。 这是 typeof
结论
Object.prototype.toString
方法; 因为这是唯一一个可依赖的方式。正如上面表格所示,typeof``typeof
instanceof
instanceof
操作符用来比较两个操作数的构造函数。只有在比较自定义的对象时才有意义。 如果用来比较内置类型,将会和 [typeof](http://bonsaiden.github.io/JavaScript-Garden/zh/#types.typeof)
一样用处不大。
比较自定义对象
function Foo() {}
function Bar() {}
Bar.prototype = new Foo();
new Bar() instanceof Bar; // true
new Bar() instanceof Foo; // true
// 如果仅仅设置 Bar.prototype 为函数 Foo 本身,而不是 Foo 构造函数的一个实例
Bar.prototype = Foo;
new Bar() instanceof Foo; // false
instanceof
new String("foo") instanceof String; // true
new String("foo") instanceof Object; // true
"foo" instanceof String; // false
"foo" instanceof Object; // false
instanceof
结论
instanceof
操作符应该仅仅用来比较来自同一个 JavaScript 上下文的自定义对象。 正如 [typeof](http://bonsaiden.github.io/JavaScript-Garden/zh/#types.typeof)
操作符一样,任何其它的用法都应该是避免的。
类型转换
弱类型语言,所以会在任何可能的情况下应用强制类型转换。
// 下面的比较结果是:true
new Number(10) == 10; // Number.toString() 返回的字符串被再次转换为数字
10 == "10"; // 字符串被转换为数字
10 == "+10 "; // 同上
10 == "010"; // 同上
isNaN(null) == false; // null 被转换为数字 0
// 0 当然不是一个 NaN(译者注:否定之否定)
// 下面的比较结果是:false
10 == 010;
10 == "-10";
ES5 提示: 以 0
开头的数字字面值会被作为八进制数字解析。 而在 ECMAScript 5 严格模式下,这个特性被移除了。
强烈推荐使用 严格的等于操作符。 虽然这可以避免大部分的问题,但 JavaScript 的弱类型系统仍然会导致一些其它问题。
内置类型的构造函数
Number
和 String
)的构造函数在被调用时,使用或者不使用 new
new Number(10) === 10; // False, 对象与数字的比较
Number(10) === 10; // True, 数字与数字的比较
new Number(10) + 0 === 10; // True, 由于隐式的类型转换
Number
作为构造函数将会创建一个新的 Number
对象, 而在不使用 new
关键字的 Number
另外,在比较中引入对象的字面值将会导致更加复杂的强制类型转换。
显式的转换为三种可能的类型之一。
转换为字符串
"" + 10 === "10"; // true
将一个值加上空字符串可以轻松转换为字符串类型。
转换为数字
+"10" === 10; // true
一元的加号操作符,可以把字符串转换为数字。
译者注:字符串转换为数字的常用方法:
+"010" === 10;
Number("010") === 10;
(parseInt("010", 10) ===
10 + // 用来转换为整数
"010.2") ===
10.2;
Number("010.2") === 10.2;
parseInt("010.2", 10) === 10;
转换为布尔型
否
!!"foo"; // true
!!""; // false
!!"0"; // true
!!"1"; // true
!!"-1"; // true
!!{}; // true
!!true; // true
核心
eval
eval
var foo = 1;
function test() {
var foo = 2;
eval('foo = 3');
return foo;
}
test(); // 3
foo; // 1
eval
只在被直接调用并且调用函数就是 eval
var foo = 1;
function test() {
var foo = 2;
var bar = eval;
bar("foo = 3");
return foo;
}
test(); // 2
foo; // 3
译者注:上面的代码等价于在全局作用域中调用 eval
,和下面两种写法效果一样:
// 写法一:直接调用全局作用域下的 foo 变量
var foo = 1;
function test() {
var foo = 2;
window.foo = 3;
return foo;
}
test(); // 2
foo; // 3
// 写法二:使用 call 函数修改 eval 执行的上下文为全局作用域
var foo = 1;
function test() {
var foo = 2;
eval.call(window, "foo = 3");
return foo;
}
test(); // 2
foo; // 3
任何情况下我们都应该避免使用 eval
函数。99.9% 使用 eval
的场景都有不使用 eval
eval
定时函数 setTimeout
和 setInterval
都可以接受字符串作为它们的第一个参数。 这个字符串总是在全局作用域中执行,因此 eval
安全问题
eval
也存在安全问题,因为它会执行任意传给它的代码, 在代码字符串未知或者是来自一个不信任的源时,绝对不要使用 eval
结论
eval
,任何使用它的代码都会在它的工作方式,性能和安全性方面受到质疑。 如果一些情况必须使用到 eval
才能正常工作,首先它的设计会受到质疑,这不应该是首选的解决方案, 一个更好的不使用 eval
undefined 和 null
undefined
。
undefined
undefined
是一个值为 undefined``undefined
,这个变量也被称为undefined
。 但是这个变量不是一个常量,也不是一个关键字。这意味着它的值可以轻易被覆盖。ES5 提示: 在 ECMAScript 5 的严格模式下,undefined
不再是 可写的了。 但是它的名称仍然可以被隐藏,比如定义一个函数名为 undefined
。undefined``undefined
- 。
return``return
- 访问不存在的属性。
- 函数参数没有被显式的传递值。
undefined
undefined
undefined
只是保存了 undefined
类型实际值的副本, 因此对它赋新值不会改变类型 undefined``undefined
做比较,我们需要事先获取类型undefined``undefined
值的改变,一个常用的技巧是使用一个传递到 匿名包装器的额外参数。 在调用时,这个参数不会获取任何值。
var undefined = 123;
(function (something, foo, undefined) {
// 局部作用域里的 undefined 变量重新获得了 `undefined` 值
})("Hello World", 42);
另外一种达到相同目的方法是在函数内使用变量声明。
var undefined = 123;
(function(something, foo) {
var undefined;
...
})('Hello World', 42);
var
译者注:这里有点绕口,其实很简单。如果此函数内没有其它需要声明的变量,那么 var
总共 4 个字符(包含一个空白字符) 就是专门为undefined
null
undefined
的使用场景类似于其它语言中的 null,实际上 JavaScript 中的 null``Foo.prototype = null
),但是大多数情况下都可以使用 undefined
自动分号插入
不强制要求在代码中使用分号,实际上可以省略它们。
自动在源代码中插入分号。
var foo = function () {}; // 解析错误,分号丢失
test();
自动插入分号,解析器重新解析。
var foo = function () {}; // 没有错误,解析继续
test();
最大的设计缺陷之一,因为它能改变代码的行为。
工作原理
下面的代码没有分号,因此解析器需要自己判断需要在哪些地方插入分号。
(function(window, undefined) {
function test(options) {
log('testing!')
(options.list || []).forEach(function(i) {
})
options.value.test(
'long string to pass here',
'and another long string to pass'
)
return
{
foo: function() {}
}
}
window.test = test
})(window)
(function(window) {
window.someLibrary = {}
})(window)
下面是解析器"猜测"的结果。
(function(window, undefined) {
function test(options) {
// 没有插入分号,两行被合并为一行
log('testing!')(options.list || []).forEach(function(i) {
}); // <- 插入分号
options.value.test(
'long string to pass here',
'and another long string to pass'
); // <- 插入分号
return; // <- 插入分号, 改变了 return 表达式的行为
{ // 作为一个代码段处理
foo: function() {}
}; // <- 插入分号
}
window.test = test; // <- 插入分号
// 两行又被合并了
})(window)(function(window) {
window.someLibrary = {}; // <- 插入分号
})(window); //<- 插入分号
注意: JavaScript 不能正确的处理 return
错误的处理。
前置括号
不会自动插入分号。
log("testing!")(options.list || []).forEach(function (i) {});
上面代码被解析器转换为一行。
log("testing!")(options.list || []).forEach(function (i) {});
log
函数的执行结果极大可能不是函数;这种情况下就会出现 TypeError
的错误,详细错误信息可能是 undefined is not a function
。
结论
绝对不要省略分号,同时也提倡将花括号和相应的表达式放在一行, 对于只有一行代码的 if
或者 else
其它
setTimeout
和 setInterval
注意: 定时处理不是 ECMAScript 的标准,它们在 DOM (文档对象模型) 被实现。
function foo() {}
var id = setTimeout(foo, 1000); // 返回一个大于零的数字
setTimeout
被调用时,它会返回一个 ID 标识并且计划在将来大约 1000 毫秒后调用 foo
函数。 foo
函数只会被执行一次。没法确保函数会在 setTimeout
全局作用域中执行,因此函数内的 [this](http://bonsaiden.github.io/JavaScript-Garden/zh/#function.this)
将会指向这个全局对象。
function Foo() {
this.value = 42;
this.method = function () {
// this 指向全局对象
console.log(this.value); // 输出:undefined
};
setTimeout(this.method, 500);
}
new Foo();
注意: setTimeout
的第一个参数是函数对象,一个常犯的错误是这样的setTimeout(foo(), 1000)
, 这里回调函数是 foo
的返回值,而不是foo
本身。 大部分情况下,这是一个潜在的错误,因为如果函数返回undefined
,setTimeout
也不会报错。
setInterval
setTimeout
只会执行回调函数一次,不过 setInterval
- 正如名字建议的 - 会每隔 X``setInterval
function foo() {
// 阻塞执行 1 秒
}
setInterval(foo, 100);
foo``foo
被阻塞的时候,setInterval
仍然在组织将来对回调函数的调用。 因此,当第一次 foo
函数调用结束时,已经有 10
处理可能的阻塞调用
setTimeout
function foo() {
// 阻塞执行 1 秒
setTimeout(foo, 100);
}
foo();
setTimeout
回调函数,而且阻止了调用指令的堆积,可以有更多的控制。 foo
手工清空定时器
clearTimeout
或者 clearInterval
函数来清除定时, 至于使用哪个函数取决于调用的时候使用的是 setTimeout
还是 setInterval
。
var id = setTimeout(foo, 1000);
clearTimeout(id);
清除所有定时器
由于没有内置的清除所有定时器的方法,可以采用一种暴力的方式来达到这一目的。
// 清空"所有"的定时器
for (var i = 1; i < 1000; i++) {
clearTimeout(i);
}
译者注:如果定时器调用时返回的 ID 值大于 1000), 因此我们可以事先保存所有的定时器 ID,然后一把清除。
eval
setTimeout
和 setInterval
也接受第一个参数为字符串的情况。 这个特性绝对不要使用,因为它在内部使用了 eval
。注意: 由于定时器函数不是 ECMAScript 的标准,如何解析字符串参数在不同的 JavaScript 引擎实现中可能不同。 事实上,微软的 JScript 会使用Function
构造函数来代替eval
function foo() {
// 将会被调用
}
function bar() {
function foo() {
// 不会被调用
}
setTimeout("foo()", 1000);
}
bar();
eval
在这种情况下不是被 直接调用,因此传递到 setTimeout
的字符串会自全局作用域中执行; 因此,上面的回调函数使用的不是定义在 bar
作用域中的局部变量 foo
。
不要在调用定时器函数时,为了向回调函数传递参数而使用字符串的形式。
function foo(a, b, c) {}
// 不要这样做
setTimeout("foo(1,2, 3)", 1000);
// 可以使用匿名函数完成相同功能
setTimeout(function () {
foo(1, 2, 3);
}, 1000);
注意: 虽然也可以使用这样的语法 setTimeout(foo, 1000, 1, 2, 3)
, 但是不推荐这么做,因为在使用对象的 属性方法时可能会出错。 (译者注:这里说的是属性方法内,this
的指向错误)
结论
绝对不要使用字符串作为 setTimeout
或者 setInterval
的第一个参数, 这么写的代码明显质量很差。当需要向回调函数传递参数时,可以创建一个匿名函数,在函数内执行真实的回调函数。setInterval
,因为它的定时执行不会被 JavaScript 阻塞。