You-Dont-Know-JS

最近在读 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 的原型链上寻找:

  1. 如果在原型链上找到了 foo 这个属性,且属性值不为 writable:false,那么 foo 这个属性会添加到 myObject 上;
  2. 如果在原型链上找到了 foo 这个属性,且属性值 writable:false,那就不会在 myObject 上添加 foo 属性;
  3. 如果在原型链上找到了 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 下执行会打印出:

async_console

很奇怪的结果,所以想要观察准确的变量值还是使用断点调试为好。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值