《你不知道的JavaScript》小结2
var anotherObject = {
a:2
};
var myObject = Object.create( anotherObject );
anotherObject.a; // 2
myObject.a; // 2
for (var k in myObject) {
console.log("found: " + k);
}
// found: a
("a" in myObject); // true
anotherObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "a" ); // false
myObject.a++;
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty( "a" ); // true
原因: ++
操作相当于 myObject.a = myObject.a + 1
。因此 ++
操作首先会通过 [[Prototype]]
查找属性 a
并从 anotherObject.a
获取当前属性值 2,然后给这个值加 1,接着用[[Put]]
将值 3 赋给 myObject
中新建的屏蔽属性 a
。
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 创建一个新原型对象
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!
原因: a1
并没有 constructor
属性,所以它会委托[[Prototype]]
链上的 Foo.prototype
。但是这个对象也没有 constructor
属性,所以它会继续委托,这次会委托给委托链顶端的 Object.prototype
。这个对象有.constructor
属性,指向内置的 Object(..)
函数。
- 关于
instanceof
instanceof
操作符的左操作数是一个普通的对象,右操作数是一个函数。a instanceof Foo; // true
中instanceof
回答的问题是:在a
的整条[[Prototype]]
链中是否有指向Foo.prototype
的对象?
这个方法只能处理对象(a
)和函数(带.prototype
引用的Foo
)之间的关系。如果你想判断两个对象(比如a
和b
)之间是否通过[[Prototype]]
链关联,只用instanceof
无法实现。参考isPrototypeOf
解决上述问题。
// 用来判断 o1 是否关联到(委托)o2 的辅助函数
function isRelatedTo(o1, o2) {
function F(){}
F.prototype = o2;
return o1 instanceof F;
}
var a = {};
var b = Object.create( a );
isRelatedTo( b, a ); // true
- 关于数组
如果字符串键值能够被强制类型转换为十进制数字的话,它就会被当作数字索引来处理。
var a = [ ];
a["13"] = 42;
a.length; // 14
- 类数组
类数组:属性要为索引(数字)属性,必须有length 属性,最好加上属性的对象
常见类数组:arguments 对象将函数的参数当作列表来访问(从ES6 开始已废止)
转换为真正的数组:数组工具函值数(如 slice(..), ES6 中的内置工具函数 Array.from(..) 等)来实现。
function foo() {
var arr = Array.prototype.slice.call( arguments );
arr.push( "bam" );
console.log( arr );
}
foo( "bar", "baz" ); // ["bar","baz","bam"]
var arr = Array.from( arguments );
- 关于
.
运算符
它是一个有效的数字字符,会被优先识别为数字常量的一部分,然后才是对象属性访问运算符。
// 无效语法:
42.toFixed( 3 ); // SyntaxError . 被视为常量 42. 的一部分
// 下面的语法都有效:
(42).toFixed( 3 ); // "42.000"
0.42.toFixed( 3 ); // "0.420"
42..toFixed( 3 ); // "42.000"
NaN
判断是否为NaN
isNaN
var a = 2 / "foo";
var b = "foo";
a; // NaN
b; "foo"
window.isNaN( a ); // true
window.isNaN( b ); // true
- ES6工具函数
Number.isNaN()
var a = 2 / "foo";
var b = "foo";
Number.isNaN( a ); // true
Number.isNaN( b ); // false
无穷除以无穷
Infinity/Infinity
// NaN
1/Infinity
// 0
-1/Infinity
// -0
-
Object.is
Object.is()
是ES6新增的用来比较两个值是否严格相等的方法,与===
的行为基本一致,区别不同:
-
+0
不等于-0
-
NaN
等于自身
Object.is('foo', 'foo'); // true
Object.is(window, window); // true
Object.is('foo', 'bar'); // false
Object.is([], []); // false
var foo = { a: 1 };
var bar = { a: 1 };
Object.is(foo, foo); // true
Object.is(foo, bar); // false
Object.is(null, null); // true
// 特例
Object.is(0, -0); // false
Object.is(0, +0); // true
Object.is(-0, -0); // true
Object.is(NaN, 0/0); // true
注意:能使用 == 和 === 时就尽量不要使用 Object.is(..) ,因为前者效率更高、更为通用。 Object.is(..) 主要用来处理那些特殊的相等比较。
- 值和引用
var a = 2;
var b = a; // b是a的值的一个副本
b++;
a; // 2
b; // 3
var c = [1,2,3];
var d = c; // d是[1,2,3]的一个引用
d.push( 4 );
c; // [1,2,3,4]
d; // [1,2,3,4]
c 和 d 则分别指向同一个复合值 [1,2,3]
的两个不同引用。请注意, c 和 d 仅仅是指向值[1,2,3]
,并非持有。所以它们更改的是同一个值(如调用 .push(4) ),随后它们都指向更改后的新值[1,2,3,4]
。
由于引用指向的是值本身而非变量,所以一个引用无法更改另一个引用的指向。
var a = [1,2,3];
var b = a;
a; // [1,2,3]
b; // [1,2,3]
// 然后
b = [4,5,6];
a; // [1,2,3]
b; // [4,5,6]
参考示例
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],不是[4,5,6,7]
我们向函数传递 a 的时候,实际是将引用 a 的一个复本赋值给 x ,而 a 仍然指向 [1,2,3]
。在函数中我们可以通过引用 x 来更改数组的值( push(4) 之后变为 [1,2,3,4]
)。但 x =[4,5,6]
并不影响 a 的指向,所以 a 仍然指向[1,2,3,4]
。我们不能通过引用 x 来更改引用 a 的指向,只能更改 a 和 x 共同指向的值。
如果要将 a 的值变为 [4,5,6,7]
,必须更改 x 指向的数组,而不是为 x 赋值一个新的数组。
function foo(x) {
x.push( 4 );
x; // [1,2,3,4]
// 然后
x.length = 0; // 清空数组
x.push( 4, 5, 6, 7 );
x; // [4,5,6,7]
}
var a = [1,2,3];
foo( a );
a; // 是[4,5,6,7],不是[1,2,3,4]
从上例可以看出,x.length = 0
和 x.push(4,5,6,7)
并没有创建一个新的数组,而是更改了当前的数组。于是 a 指向的值变成了 [4,5,6,7]
。
- 封装对象
var a = new Boolean( false );
if (!a) {
console.log( "Oops" ); // 执行不到这里
}
var a = "abc";
var b = new String( a );
var c = Object( a );
typeof a; // "string"
typeof b; // "object"
typeof c; // "object"
b instanceof String; // true
c instanceof String; // true
Object.prototype.toString.call( b ); // "[object String]"
Object.prototype.toString.call( c ); // "[object String]"
- 拆封
得到封装对象中的基本类型值,可以使用 valueOf() 函数:
var a = new String( "abc" );
var b = new Number( 42 );
var c = new Boolean( true );
a.valueOf(); // "abc"
b.valueOf(); // 42
c.valueOf(); // true
var a = new String( "abc" );
var b = a + ""; // b的值为"abc"
typeof a; // "object"
typeof b; // "string"
- 关于数组
var a = new Array( 3 );
var b = [ undefined, undefined, undefined ];
var c = [];
c.length = 3;
a; // [empty × 3]
b; // [undefined, undefined, undefined]
c; // [empty × 3]
a.join( "-" ); // "--"
b.join( "-" ); // "--"
a.map(function(v,i){ return i; }); // [empty × 3]
b.map(function(v,i){ return i; }); // [ 0, 1, 2 ]
以上均为Chrome上的测试结果
下图为Firefox测试结果
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 str;
}
var a = new Array( 3 );
fakeJoin( a, "-" ); // "--"
- 将原型作为默认值
typeof Function.prototype; // "function"
Function.prototype(); // 空函数!
RegExp.prototype.toString(); // "/(?:)/" —— 空正则表达式
"abc".match( RegExp.prototype ); // [""]
Array.isArray( Array.prototype ); // true
Array.prototype.push( 1, 2, 3 ); // 3
Array.prototype; // [1,2,3]
// 需要将Array.prototype设置回空,否则会导致问题!
Array.prototype.length = 0;
Function.prototype 是一个函数, RegExp.prototype 是一个正则表达式,而 Array.
prototype 是一个数组。
即:
Object.prototype.toString.call(Function.prototype)
// "[object Function]"
Object.prototype.toString.call(String.prototype)
// "[object String]"
Object.prototype.toString.call(Number.prototype)
// "[object Number]"
Object.prototype.toString.call(Boolean.prototype)
// "[object Boolean]"
Object.prototype.toString.call(Array.prototype)
// "[object Array]"
但是请注意下面的情况:
Object.prototype.toString.call(RegExp.prototype)
// "[object Object]"
Object.prototype.toString.call(/!/)
// "[object RegExp]"
Object.prototype.toString.call(new RegExp())
// "[object RegExp]"
考虑到上面的特性,这对未赋值变量是一个很好的默认值
function isThisCool(vals = Array.prototype, fn = Function.prototype, rx = RegExp.prototype) {
return rx.test(vals.map( fn ).join( "" ));
}
isThisCool(); // true
isThisCool(["a","b","c"], function(v){ return v.toUpperCase(); }, /D/); // false
这种方法的一个好处是.prototypes
已被创建并且仅创建一次。相反,如果将 []
、function(){}
和 /(?:)/
作为默认值,则每次调用 isThisCool()
时它们都会被创建一次(具体创建与否取决于 JavaScript 引擎,稍后它们可能会被垃圾回收),这样无疑会造成内存和 CPU 资源的浪费。
另外需要注意的一点是,如果默认值随后会被更改,那就不要使用 Array.prototype 。上例中的 vals 是作为只读变量来使用,更改 vals 实际上就是更改 Array.prototype ,而这样会导致前面提到过的问题!