1.对象
1.1 对象使用和属性
JavaScript中所有变量都是对象,除了null和undefined
1.2 对象作为数据类型
JavaScript对象可以作为哈希表使用,主要用来保存命名的键和值的对应关系
1.3 访问属性
点操作符和中括号操作符
中括号操作符在下面2种情况下依然有效 1.动态设置属性 2.属性名不是一个有效的变量名
1.4 删除属性
删除属性的唯一方法是使用delete操作符;设置属性为undefined或者null并不能真正的删除属性,只是移除了属性和值的关联
1.5 属性名的语法
对象的属性名可以使用字符串或者普通字符(非关键词)声明。
2.原型
2.1 原型继承
注:简单的使用Bar.prototype = Foo.prototype将会导致两个对象共享相同的原型。
function Foo(){
this.value = 42;
}
Foo.ptototype = {
method : function() {}
};
function Bar() {}
//设置Bar的prototype属性为Foo的实例对象
Bar.prototype = new Foo();
Bar.prototype.foo = "Hello World";
//修正Bar.prototype.constructor为Bar本身
Bar.prototype.constructor = Bar;
注:不要使用Bar.prototype = Foo, 因为这不会执行Foo的原型,而是指向函数Foo。因此原型链将会回溯到Function.prototype而不是Foo.prototype。
2.2 属性查找
查找对象属性时,JavaScript会向上遍历原型链,直到找到给定名称的属性为止。
到查找到达原型链的顶部(Object.prototype),但是仍然没有找到指定的属性,就会返回undefined。
2.3 原型属性
当原型属性用来创建原型链时,可以把任何类型的值赋给它。然而将原子类型赋给prototype的操作将会被忽略。
2.4 性能
如果一个属性在原型链的顶端,则对于查找时间将带来不利影响。
当使用 for - in 循环遍历对象的属性时,原型链上的所有属性都将被访问。
2.5 扩展内置类型的原型
一个错误特性被经常使用,那就是扩展Object.prototype或者其他内置类型的原型对象。
这种技术会破坏封装。
扩展内置类型的唯一理由是为了和新的JavaScript保持一致。比如Arrary.forEach。
3.函数
3.1 函数声明
函数声明会在执行前被解析(hoisted),因此它存在于当前上下文的任意一个地方,即使在函数定义体的上面被调用也是对的。
foo();
function foo(){}
3.2 函数赋值表达式
foo;
foo();
var foo = function(){};
由于var定义了一个声明语句,对变量foo的解析是在代码运行之前,因此foo变量在代码运行时已经被定义过了。
但是由于赋值语句只在运行时执行,因此在相应代码执行之前,foo的值缺省为undefined。
3.3 hasOwnProperty函数
hasOwnProperty是JavaScript中唯一一个处理属性但是不需要查找原型链的方法。
JavaScript不会保护hasOwnProperty被非法占用,因此如果一个对象碰巧存在这个属性,就需要使用外部的hasOwnProperty函数来获取正确的结果。
{}.hasOwnProperty.call(foo, ‘bar‘);
4.this的工作原理
4.1 全局范围
当在全部范围内使用this,它将会指向全局对象。(浏览器中运行的JavaScript脚本,这个全局对象是window)
4.2 函数调用
foo();
此时this指向全局对象。
ES5 注意: 在严格模式下(strict mode),不存在全局变量。这种情况下this将会是undefined。
4.3 方法调用
this指向调用该方法的对象
4.4 调用构造函数
this指向新创建的对象。
4.5 call和apply
call 或者 apply 方法时,函数内的 this将会被显式设置为函数调用的第一个参数,若为null则指向全局对象
4.6 方法的赋值表达式
将一个方法赋值给一个变量。
var test = someObject.methodTest;
test();
此时,test就像一个普通的函数被调用;因此,函数内的this将不再被指向到someObject对象。(晚绑定)
5.闭包和引用
5.1 模拟私有变量
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。
5.2 不可以在外部访问私有变量
因为JavaScript中不可以对作用域进行引用或赋值,因此没有办法在外部访问count变量。唯一的途径就是通过那两个闭包。
5.3 循环中的闭包
for(var i = 0; i < 10; i++ ){
setTimeout(function(){
console.log(i);
}, 1000);
}
当console.log被调用的时候,匿名函数保持对外部变量i的引用,此时for循环已经结束,i的值被修改成10。
为了得到想要的结果,需要在每次循环中创建变量i的拷贝。
5.3 避免引用错误
使用自执行匿名函数
for(var i = 0; i < 10; i++){
(function(e){
setTimeout(function(){
console.log(e);
}, 1000);
})(i);
}
6.arguments对象
6.1 基本概念
JavaScript中每个函数内都能访问arguments。它维护着所有传递到这个函数中的参数列表。
通过var关键字定义arguments或者将arguments声明为一个形式参数,都将导致原生的arguments不会被创建。
arguments变量不是一个数组,尽管在语法上有个数组相关的属性length。
6.2 转化为数组
Array.prototype.slice.call(arguments)
6.3 传递参数
function foo(){
bar.apply(null, arguments);
}
function bar(a, b, c){
//some work
}
6.4 性能真相
arguments对象总会被创建,除了两个特殊情况 - 作为局部变量声明和作为形式参数。
使用arguments.callee会显著的影响现代JavaScript引擎的性能。
ES5提示:在严格模式下,arguments.callee会报错TypeError,因为它已经被废除了。
7.构造函数
7.1基本概念
在构造函数内部,this指向新创建的对象Object。这个新创建的对象的prototype被指向到构造函数的prototype。
被调用的函数没有显示的return表达式,则隐式的会返回this对象。
显示的return表达式将会影响返回结果,但仅限于返回的是一个对象。
function Bar(){
return 2;
}
new Bar(); //返回新创建的对象 new Bar().constructor === Bar
function Foo(){
this.value = 2;
return {
foo : 1
};
}
new Foo(); //返回的对象 {foo:1}
7.2 通过工厂模式创建新对象
function Foo(){
var obj = {};
obj.value = "blue";
var privateVal = 2;
obj.someMethod = function(value){
this.value = value;
}
obj.getPrivate = function(){
return privateVal;
}
return obj;
}
优点:充分利用私有变量,比起使用构造函数方式不容易出错。
缺点:占用更多的内存,新创建的对象不能共享原型上的方法。
为了实现继承,工厂方法需要从另外一个对象拷贝所有属性,或者把一个对象作为新创建对象的原型。
放弃原型链仅仅是因为防止遗漏new带来的问题,这似乎和语言本身的思想想违背。
8.作用域与命名空间
8.1 函数作用域
JavaScript不支持块级作用域,而仅仅支持函数作用域。
注意: 如果不是在赋值语句中,而是在return表达式或者函数参数中,{...}
将会作为代码段解析, 而不是作为对象的字面语法解析。如果考虑到自动分
号插入,这可能会导致一些不易察觉的错误。如果return对象的左括号和return不在一行上就会出错。
8.2 局部变量
两种方式声明,一个事作为函数参数,另一个是通过var关键字声明。
8.3 变量声明提升
JavaScript会提升变量声明。这意味着var表达式和function声明都将会被提升到当前作用域的顶部。
8.4 名称解析顺序
当访问函数内的foo变量时,JavaScript会按照下面顺序查找:
1.当前作用域内是否有var foo的定义。
2.函数形式参数是否有使用foo名称的。
3.函数自身是否叫做foo。
4.回溯到上一级作用域,然后从 1 重新开始
8.5 命名空间
通过立即执行的匿名函数解决命名冲突的问题。
(function(){
//do some work
})();
9.数组
9.1 遍历
使用for - in循环,会查询对象原型链上的所有属性,因此需要使用hasOwnProperty函数来过滤,比普通for循环慢上好几倍。
9.2 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 是一个变量,注意是变量不是关键字,因此上面两个结果的意义是
完全不相同的。
9.3 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); 这种调
用方式),并且这个参数是数字,构造函数会返回一个 length 属性被设置为
此参数的空数组。 需要特别注意的是,此时只有 length 属性被设置,真正的
数组并没有生成。
10.相等于比较
10.1 等于操作符
等于操作符由两个等号组成:==
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
此外,强制类型转换也会带来性能消耗,比如一个字符串为了和一个数组进行比较,必须事先被强制转换为数字。
10.2 严格的等于操作符
严格的等于操作符由三个等号组成:===
不想普通的等于操作符,严格的等于操作符不会进行强制类型转换。
"" === "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
10.3 比较对象
虽然 == 和 === 操作符都是等于操作符,但是当其中有一个操作数为对象时,行为就不同了。
{} === {}; // false
new String(‘foo‘) === ‘foo‘; // false
new Number(10) === 10; // false
var foo = {};
foo === foo; // true
这里等于操作符比较的不是值是否相等,而是是否属于同一个身份;也就是说,
只有对象的同一个实例才被认为是相等的。 这有点像 Python 中的 is 和 C
中的指针比较。
11.typeof操作符
11.1 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
Class一列表示对象的内部属性[[Class]]的值。
为了获取对象的[[Class]],我们需要使用定义在 Object.prototype上的方法toString。
11.2 对象的类定义
JavaScript 标准文档中定义: [[Class]] 的值只可能是下面字符串中的一个:
Arguments, Array, Boolean, Date, Error, Function, JSON, Math, Number, Object, RegExp, String。
JavaScript标准文档只给出了一种获取[[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
ES5 提示: 在 ECMAScript 5 中,为了方便,对 null 和 undefined 调用
Object.prototype.toString方法,其返回值由Object变成了Null和Undefined。
12.instanceof操作符
12.1 基本概念
instanceof操作符用来比较两个操作数的构造函数。只有在比较自定义的对象时才有意义。如果用来比较内置类型,将会和typeof操作符一样用处不大。
12.2 比较自定义对象
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
12.3 instanceof比较内置类型
new String(‘foo‘) instanceof String; // true
new String(‘foo‘) instanceof Object; // true
‘foo‘ instanceof String; // false
‘foo‘ instanceof Object; // false
13.类型转换
13.1 内置类型的构造函数
new Number(10) === 10; // False, 对象与数字的比较
Number(10) === 10; // True, 数字与数字的比较
new Number(10) + 0 === 10; // True, 由于隐式的类型转换
使用内置类型Number作为构造函数将会创建一个新的Number对象,而在不使用new关键字的Number函数更像是一个数字转换器.
最好的选择是把要比较的值显式的转换为三种可能的类型之一。
13.2 转换为字符串
‘‘ + 10 === ‘10‘; // true
将一个值加上空字符串可以轻松转换为字符串类型。
13.3 转换为数字
+‘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
13.4 转换为布尔型
通过使用否操作符两次,可以把一个值转换为布尔型。
!!‘foo‘; // true
!!‘‘; // false
!!‘0‘; // true
!!‘1‘; // true
!!‘-1‘ // true
!!{}; // true
!!true; // true
14.eval
14.1 为什么不要使用eval
在任何情况下我们都应该避免使用eval函数。99.9%使用eval的场景都有不使用eval的解决方案。
14.2 安全问题
eval 也存在安全问题,因为它会执行任意传给它的代码,在代码字符串未知或者是来自一个不信任的源时,绝对不要使用eval函数。
15.undefined 和 null
15.1 undefined的值
undefined是一个值为undefined的类型。
下面的情况会返回 undefined 值:
? 访问未修改的全局变量 undefined。
? 由于没有定义 return 表达式的函数隐式返回。
? return 表达式没有显式的返回任何内容。
? 访问不存在的属性。
? 函数参数没有被显式的传递值。
? 任何被设置为 undefined 值的变量。
15.2 处理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);
15.3 使用 null
JavaScript 中的 undefined 的使用场景类似于其它语言中的 null,实际上JavaScript 中的 null 是另外一种数据类型。
它在 JavaScript 内部有一些使用场景(比如声明原型链的终结Foo.prototype = null ),但是大多数情况下都可以使用 undefined 来代替。
16.setTimeout 和 setInterval
16.1 定时器
基于JavaScript引擎的计时策略,以及本质上的单线程运行方式,所以其它代码的运行可能会阻塞此线程。因此没法确保函数会在setTimeout指定的时刻被调用。
作为第一个参数的函数将会在全局作用域中执行,因此函数内的 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也不会报错。
16.2 setInterval的堆调用
当回调函数的执行被阻塞时,setInterval 仍然会发布更多的毁掉指令。在很小的定时间隔情况下,这会导致回调函数被堆积起来。
function foo(){
// 阻塞执行 1 秒
}
setInterval(foo, 100);
上面代码中,foo 会执行一次随后被阻塞了一分钟。
在 foo 被阻塞的时候,setInterval 仍然在组织将来对回调函数的调用。 因此,当第一次 foo 函数调用结束时,已经有10次函数调用在等待执行。
16.3 处理可能的阻塞调用
最简单也是最容易控制的方案,是在回调函数内部使用 setTimeout 函数
function foo(){
// 阻塞执行 1 秒
setTimeout(foo, 100);
}
foo();
16.4 手工清空定时器
可以通过将定时时产生的 ID 标识传递给clearTimeout或者clearInterval函数来清除定时。
16.5 隐藏使用 eval
setTimeout 和 setInterval 也接受第一个参数为字符串的情况。 这个特性绝对不要使用,因为它在内部使用了 eval。
详情可见:JavaScript秘密花园