注意:本文章是个人《You Don’t Know JS》的读书笔记。
在看backbone
源码的时候看到这么一小段,看上去很小,其实忽略了也没有太大理解的问题。但是不知道为什么,我觉得心里很难受。所以我觉得一定要真正解决这个问题。这个问题就是原型。
就是下面这段代码:
var Surrogate = function(){ this.constructor = child; };
Surrogate.prototype = parent.prototype;
child.prototype = new Surrogate;
看懂了这一段简单的代码了吗?
其实就着backbone
的注释,理解完全没有问题。但是之后我不小心深究了一下,突然发现自己对于这一个问题的理解还很不透彻。很惭愧,写了好一阵子的JavaScript
,但是一直都是以实用主义的角度来写的。很多时候真的把JavaScript
当成传统有class
的语言来写了。但是JavaScript
是一门很“狡猾”的语言,因为它的很多关键字仿佛都在透露自己是传统面向对象语言,instance
,constructor
,new
等等。然而事实上并不是。解决这个问题的方法就是读书,我选了《You Don’t Know JS》。我快速过了相关的那一本,被这本书深深震撼到。本文是个人的学习笔记,也希望能帮助到你。
this
this
是函数执行的时候,内部产生的一个内部对象。在情况复杂的时候,this的指向会比较难把握。
this的指向常常不明,在书中主要列举了两种情况的混淆和四条规则,掌握了这两者,this就会比较明晰了。(还有很多很复杂的情况需要细致的分析)
两种混淆
1,this
不是函数对象本身。函数本身也是一个对象,但是给这个对象添加属性并不能影响this
。除非foo.call(foo, i)
,这句代码的含义是:在foo对象上调用foo函数,并传递参数i
。这是一个特殊情况。
2,this
不是函数内部的作用域。因此在函数内部申明的变量如果不加其他操作,和this
根本不会有任何关系。
四条规则
这四条规则只有一个中心,就是call-site
。本质上就是指this
决定于调用的对象而不是在哪里声明。
1,直接调用函数。函数内部this
会指向全局。this
此时为window
。闭包调用亦是如此。
2,内部绑定调用。如obj.foo()
。此时foo
中的this
指的就是obj
对象。注意obj.foo()
,不管前面是不是还有其他调用,但是都不会改变this
此时为obj
。
3,直接绑定调用。主要是用了call
和apply
来调用。除此之外还有先bind
在调用的方式,这种方式非常强。直接绑定调用的this
就是指定的那个参数的对象。
4,new
方式调用。var bar = new foo()
这一句代码的作用之一就是把foo
函数中的this
全部赋到bar
上作为新的bar
对象的属性。
优先级
1,一般来说直接绑定调用会比内部绑定调用有更高优先级。
2,new
在一定情况下能“覆盖”bind
调用。用这个特性可以实现函数currying
化。(函数传入多个参数,在bind的时候传入固定的变量)
function foo(val) {
this.val = val;
}
var obj = {};
var bar = foo.bind(obj);
bar("p1");
var baz = new bar("p2")
console.log(baz.val); // p2
new & Object.create
JavaScript
没有类,也不能创建实例。但是语法上确实非常非常面向对象,让人误解。new
就是总被误解。
var bar = new foo();
下面详述这一句代码执行的效果。这非常重要。
1,执行foo()
函数。是“构造方式调用”。注意,JavaScript
里面并没有真正的构造函数。
2,连接foo.prototype
和新对象bar
的[[prototype]]
。(相当于把foo.prototype
整体搬到(引用)bar.__proto__
里面)
3,把foo
中的内部对象this
的属性给bar
。
4,foo
如果有返回对象就返回那个对象,没有就自动返回一个新的对象。对象特征如2,3所述。
事实上,new
做得事太多,很多时候令人讨厌。
注意:bar
和foo
函数对象本身的属性没有关系。
在经典的教材上,JavaScript
的“继承”常常使用下面的代码:
Bar.prototype = new Foo();
这句代码很经典,但是并不好。它固然能够完成原型链的连接,但是也产生了很多不必要的麻烦。Bar.prototype
上会带上Foo
的this
,副作用大。因此书上建议是这样做来完成继承的:
Bar.prototype = Object.create(Foo.prototype);
A = Object.create(B)
的作用是把B
对象的属性添加到A
的__proto__
上面,然后A
的[[prototype]]
与B
的连接。有个比较“直观”的说法,就是把B
的“属性”,添加到A.__proto__
上面,然后把B.__proto__
也添加进去(原型链延长的不严谨说法)。试着在chrome
里面console
一下,就会发现此时的A
只有一个__proto__
属性,里面是B
的属性和B.__proto__
。这样就很好地完成了“继承”,而且也没有副作用了。下面提供一个降级写法(原理很简单。按照之前的几条规则可以很轻易地读懂):
// Object.create
if(!Object.create) {
Object.create = function(o) {
function F(){};
F.prototype = o;
return new F();
}
}
还需要注意的是constructor
属性。如果对象是函数,这个属性会自动出现在函数的prototype
里面,但有时候会被覆盖。
原型
prototype
是函数对象的一个属性。其实可以理解成比较特殊的一个属性,一个对象。这个属性在用构造器方式调用(new
)的时候就用来连接新对象。连接到最后就是Object.prototype
,这是每一个对象(不包括null
)中__proto__
的尽头,原型链的尽头。可以想象,对象的声明是new Object()
,那么新的对象原型链自然就会有Object.prototype
了。如果想摆脱这个Object.prototype
,可以用Object.create(null)
。
经常有这种情况,调用一个对象的属性的时候,如果在本身找不到就会沿着原型链找。直观而不严谨地说,就是从__proto__
一直找下去。这也揭露了a instanceof foo
的本质。就是沿着原型链找,看看在a
的原型链上有没有foo.prototype
的存在,有就返回true
。instanceof
这个英文很误导,因为JavaScript
里面根本上就算不上有实例这样的东西。
OLOO
作者对于JavaScript
的面向对象有一个非常惊艳的解决方案,在那之前先看看以前的方案是怎么做的。
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();
一图胜千言:
作者给出的解决方案OLOO
(objects-linked-to-other-objects
),没有麻烦的new
,没有虚伪的constructor
,没有混淆视线的call
, apply
, bind
,原型链连接不再赤裸裸。感觉真心很棒(注意一点,JavaScript
对象的方法其实是不是“属于”一个对象值得商榷,因为所谓方法其实和这个对象关系并没有想象中大):
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();
仍然是一图胜千言:
最后来讲讲这个:
var Surrogate = function(){ this.constructor = child; };
Surrogate.prototype = parent.prototype;
child.prototype = new Surrogate;
很好懂了吧。构造形式调用Surrogate
函数,把this
上的属性交给child.prototype,这里是constructor
;把Surrogate.prototype
的内容搬到child.prototype.__proto__
上面。而Surrogate.prototype
引用自parent.prototype
。所以child
是这样的一个函数:以child
为构造函数,parent.prototype
为原型。
总结:
这个问题是老生常谈的问题了,但是我觉得对于每一个写JavaScript
的人来说,这个问题是永远避不开的。最后再次推荐《You Don’t Know JS》这一套开源书。可以毫不夸张地说,这是自红宝书和犀牛书之后讲JavaScript
的最好书了。深度深得恰到好处,语言和例子清晰简明,我还要继续学习。
有关 this & Object Prototypes
,书的内容远远比我这篇文章丰富精彩,去读吧。(我也要复反复读)
如果有任何错误请轻喷,相互学习~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。