最近在读 You-Dont-Know-JS 这本书,作者由浅及深地讲解了 JS 的基本语法,还提出了很多不为人知的细节。感觉收获颇多。
下面是我自己总结的一些书中的知识,主要是自己以前不太了解的一些细节。
行为代理
在 JS 中,我们使用 prototype 来实现面向对象中的“继承”。但作者认为,就是因为我们把 prototype 机制描述为类或是继承,所以才导致了 JS 中的 prototype 难以理解。
JS中的 prototype 机制本质上来讲就是将对象连接到其他对象 (objects being linked to other objects)。使用行为代理 (behavior delegation) 来描述 prototype 机制更加合适,而不是类。
作者定义了一种新的代风格 “OLOO” (objects-linked-to-other-objects), 来区别与 “OO” (object-oriented).
下面来看看这两种风格的代码具体是怎么表现的。
function Foo(who) {
this.me = who;
}
Foo.prototype.identify = function() {
return "I am " + this.me;
};
function Bar(who) {
Foo.call( this, who );
}
Bar.prototype = Object.create( Foo.prototype );
Bar.prototype.speak = function() {
alert( "Hello, " + this.identify() + "." );
};
var b1 = new Bar( "b1" );
var b2 = new Bar( "b2" );
b1.speak();
b2.speak();
上面是经典的 OO 风格的代码,子类 Bar 继承父类 Foo,将 Bar.prototype 指向 Foo.prototype,然后实例化为 b1 和 b2。
下面使用 OLOO 风格实现相同功能的代码
var Foo = {
init: function(who) {
this.me = who;
},
identify: function() {
return "I am " + this.me;
}
};
var Bar = Object.create( Foo );
Bar.speak = function() {
alert( "Hello, " + this.identify() + "." );
};
var b1 = Object.create( Bar );
b1.init( "b1" );
var b2 = Object.create( Bar );
b2.init( "b2" );
b1.speak();
b2.speak();
可以看到 OOLO 风格的代码中只是将对象连接到其他的对象,而不用处理一些令人疑惑的概念,比如 calss, constructor, prototype, new.
更加详细的介绍可以看 github 上面的原文。
Thenable
我们都知道 Promise 可以使用 resolve() 来解析普通的变量或是 Promise 对象。例如:
Promise.resolve(2).then(console.log);
// => 2
Promise.resolve(new Promise(resolve =>
resolve(2)
)).then(console.log);
// = > 2
但更精确地说 Promise.resolve() 不是能解析 Promise 对象,而是嗯那个解析 thenable 对象,系包含 then() 函数的对象。例如:
let thenable = {
then: function (resolve, reject) {
resolve(2)
}
};
Promise.resolve(thenable).then(console.log);
// => 2
变量提升
考虑下面代码,作者写的是在 ES6 以前的环境中,因为变量提示的缘故,所以 if() 中无论是 true 还是 false,第二个始终会覆盖第一个于是打印出 2。 而在 ES6 环境中,则会抛出 ReferenceError。
但我实际测试情况是在最新的 chrome 浏览器中,如果 true 输出1,false 则输出2. 也就是说输出结果和直观感觉是一样的。
if(true){
function foo(){ console.log(1) }
}else{
function foo(){ console.log(2) }
}
foo();
bind and new
我们知道 new 和 bind 都会绑定 this,那么 bind 和 new 一起用会发生什么?
看下面的代码
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a );
// => 2
var baz = new bar( 3 );
console.log( obj1.a );
// => 2
console.log( baz.a );
// => 3
bar 已经绑定在 obj1 上了,但是当 new bar(3) 并没有改变 obj1.a 的值,而是改变了 baz.a 的值。 也就是 new 操作改变了使用 bind 绑定的 this。
这是因为 new 操作会新建一个对象,并将 this 指向这个对象,然后调用构造函数。类似于以下的操作:
var obj = {};
obj.__proto__ = foo.prototype;
foo.call(obj);
setter
当我们对一个设置属性时,并不是简单地增加一个新属性,比如下面代码:
myObject.foo = "bar";
如果 myObject 已经有一个 foo 的属性,那么就会直接覆盖。
如果没有这个属性,则会往 myObject 的原型链上寻找:
- 如果在原型链上找到了 foo 这个属性,且属性值不为 writable:false,那么 foo 这个属性会添加到 myObject 上;
- 如果在原型链上找到了 foo 这个属性,且属性值 writable:false,那就不会在 myObject 上添加 foo 属性;
- 如果在原型链上找到了 foo 这个属性,且是一个 setter 函数,那么就是调用这个setter 函数。 foo 不会添加到 myObject.
注意下面代码中 anotherObject.b = 4 没有生效
var anotherObject = {
a: 2
};
var myObject = Object.create( anotherObject );
Object.defineProperty(anotherObject,'b',{
value:1,
writable:false,
});
anotherObject.b = 4;
console.log(anotherObject.b);
// => 1
下面代码中 anotherObject.b = 4 也没有生效
var anotherObject = {
a: 2
};
var myObject = Object.create( anotherObject );
Object.defineProperty(anotherObject,'b',{
get:function () {
return 3;
}
});
anotherObject.b=4;
console.log(anotherObject.b);
// => 3
但如果想要将 foo 属性添加到 myObject 中,可以使用 Object.defineProperty() 的方法。
再看下面代码
var anotherObject = {
a: 2
};
var myObject = Object.create( anotherObject );
anotherObject.a; // 2
myObject.a; // 2
myObject.a++; // oops, implicit shadowing!
anotherObject.a; // 2
myObject.a; // 3
执行 myObject.a++ 的结果实际是 myObject.a = myObject.a + 1 ,也就是获取 myObject.a 是从原型链得到2,而赋值是在 myObject.a 操作的。
spread and curry
使用 apply 可以展开参数,类似与 ES6 中的展开运算符。
使用 bind 传入第二个参数可以实现类似柯里化的操作
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// spreading out array as parameters
foo.apply( null, [2, 3] );
// => a:2, b:3
// currying with `bind(..)`
var bar = foo.bind( null, 2 );
bar( 3 );
// => a:2, b:3
隐式转换
看下面代码,是不是很奇怪
var a = { b: 42 };
var b = { b: 43 };
a < b; // false
a == b; // false
a > b; // false
a <= b; // true
a >= b; // true
在使用 < >比较两个对象时,会首先调用 ToPrimitive(toString 或 valueOf)。所以 a 会变成 [object Object],b 也是 [object Object]。
在使用 == 比较两个对象时,直接比较两个对象的地址,因此也是 false。
而在 a<= b 时,实际是先比较 a > b,然后对其结果取反,所以是true。
labeled statements
仔细看下面代码 bar:{…}, 这里并不是创建一个对象字面量。而是将一个代码块打上标记 (labeled statements) 。
打上标记的代码块可以使用 break 来结束执行。如下代码,在 break bar 后面的代码不会被执行。
function foo() {
// `bar` labeled-block
bar: {
console.log( "Hello" );
break bar;
console.log( "never runs" );
}
console.log( "World" );
}
foo();
// => Hello
// => World
async console
浏览器中的 console 实际是异步执行的,因此有时候打印出来的变量跟实际的变量会有所不同。
var a = {
index: 1
};
// later
console.log( a ); // ??
// even later
a.index++;
上面的代码在 chrome 下执行会打印出:
很奇怪的结果,所以想要观察准确的变量值还是使用断点调试为好。