使用原型链来构建项目架构
原型和原型链
一点废话
现在网上关于原型链这一部分的博客大多是从类继承模型开始说的。因为 javascript 的继承模型区别于很多传统语言的类继承,它使用的是 prototype 原型模型模拟出继承的效果。但是我是做IC前端设计出身的,想我这种本来就对与类继承不熟悉的人说这个区别意义也不大。我之前仅仅接触过面向过程的编程,从JAVA,pathon之类的高级语言过渡到javascript上面自然而然就会有提到两者的区别。对于没有接触过的面向对象编程的人来说的话最好先能够对对象,以及面向对象的编程又一个大致的了解。因为只有先对“对象”在一个项目里面的意义有所了解,才会明白我们为什么需要用到用原型链或者其他的什么方式来构建项目架构。到但是如果我要开始从对象开始说的话,这篇文章就会过长,所以我假设正在读这篇文章的你有着还不错的逻辑思维能力,但是对于高级语言这点破事又不太清楚。那么,我们开始吧。
原型链
如果你之前有过编程经验。那么,我相信你一定知道我们会把一些反复用到的功能封装成一个函数。现在,除了函数以外,在面向对象的语言里面,还引入了对象这个概念。对于 javascript 来说除了 null 和 undefined 以外都是对象。每一个对象都会有对应的属性或者方法。那么,当我们访问对象的属性的时候,如果这个对象不具有这种属性,那么编译器就会顺着原型链向上寻找,一个对象除了会拥有自己的属性以外,还会继承来自原型链上层的父级对象的属性,所以编译器会一直顺着原型链向上寻找,直到找到那个属性或者到达原型链末尾。但是在实际操作中这样做会实际上非常消耗资源,因为这需要遍历整个原型链,所以很多时候还是使用hasOwnProperty 方法,这是官方钦定的唯一一种不需要遍历整个原型链就能对属性进行处理的方法。
上面说的这种依次向上寻找的寻找属性的方法就是 javascript 中的继承方式
//// 假定有一个对象 o, 其自身的属性(own properties)有 a 和 b:
// {a: 1, b: 2}
// o 的原型 o.[[Prototype]]有属性 b 和 c:
// {b: 3, c: 4}
// 最后, o.[[Prototype]].[[Prototype]] 是 null.
// 这就是原型链的末尾,即 null,
// 根据定义,null 没有[[Prototype]].
// 综上,整个原型链如下:
// {a:1, b:2} ---> {b:3, c:4} ---> null
console.log(o.a); // 1
// a是o的自身属性吗?是的,该属性的值为1
console.log(o.b); // 2
// b是o的自身属性吗?是的,该属性的值为2
// o.[[Prototype]]上还有一个'b'属性,但是它不会被访问到.这种情况称为"属性遮蔽 (property shadowing)".
console.log(o.c); // 4
// c是o的自身属性吗?不是,那看看o.[[Prototype]]上有没有.
// c是o.[[Prototype]]的自身属性吗?是的,该属性的值为4
console.log(o.d); // undefined
// d是o的自身属性吗?不是,那看看o.[[Prototype]]上有没有.
// d是o.[[Prototype]]的自身属性吗?不是,那看看o.[[Prototype]].[[Prototype]]上有没有.
// o.[[Prototype]].[[Prototype]]为null,停止搜索,
// 没有d属性,返回undefined
上面提到的是关于原型链是一种理想化的模型。便于理解,在通常的情况下,每个函数都有一个原型属性 prototype 指向自己的原型,而由这个函数创建的对象也有一个 _proto_ 属性指向这个原型。上面说的有点绕,我从另一个角度来说明原型链盒原型吧。先从下面这张图开始说
我们可以看到这里面有 _proto_ 和 prototype 这两个东西。如果你有使用过chrome 的develop tools 或者vscode进行单步调试的经验的话你会经常看到 *_proto_* 这个字段。那么,这个到底是什么意思呢
_proto_ 和 protptype 两个有什么关系与区别呢?
先看 _proto_ 开始说起
每一个JS对象一定对应一个原型对象,并且从原型对象那里继承属性和方法。(重点)
这话不是我说的,看下面
Every JavaScript object has a second JavaScript object (or null ,
but this is rare) associated with it. This second object is known as a prototype, and the first object inherits properties from the prototype.
看完了以后再看下面这个例子
var one = {x: 1};
var two = new Object();
one._proto_ === Object.protopyte //true
two._proto_ === Object.prototype //true
one.toSting === one._protpto_.toString //true
在上面两个例子里面,one 和 two 两个对象的原型对象全等于对自己当前对象的 _proto_ 属性。更进一步来说,one 的自己的继承来自己原型的方法的时候也可以用 _proto_ 来找到。但是这也引入了一个新问题。为什么one 和 two 的原型对象就是Object.protopyte,还有 Object.protopyte 究竟是个啥?
上面这个问题先放一下,等我讲完了prototype再放到一起看
首先,prototype 和 _proto_ 的第一个区别就在于:每一个对象都会有一个 _proto_ 属性来标示自己所继承的原型。但是函数才会有 prototype 属性。当我们创建函数的时候,JS 会为这个函数追加一个 prototype 属性。当我们尝试把这个函数当成一个构造函数来调用的时候,那么 JS 就会创建这个构造函数的实例,这个事例会继承构造函数 prototype 的所有属性和方法。同时实例会通过 _proto_ 指向构造函数的 prototype 。
于是 JS 就是这样通过 _proto_ 和 prototype 来实现原型链。
构造函数就是通过 prototype 来保存要共享给实例的属性和方法。
对象的 _proto_ 总是指向自己的构造函数的 prototype 。 大概可以这样描述一下
obj._proto_._proto_ === Constructor.prototype
之类的。
最后再补充一下,原型链的顶端就是 Object.prototype 。因为在之前的图片里面 Object.prototype 的上层 _proto_ 指向的是 null
君与this与apply与call那点破事
在讨论完原型链之后,还有一个绕不开的问题就是 this 的指向问题,还有 apply 和 call 到底是咋回事的问题。
this
最开始接触到 this 是在廖雪峰的教程里面,当时只记得 this 有设计缺陷啥的,理解也不是特别深刻。我现在常识对之前的问题祖宗一个总结,尝试用一种简单的方式并且全面的把这个部分讲清楚。
如果你尝试寻找网上关于this的资料,那么很大的概率最终会找到这么一篇文章 ,说真的看了这篇文章之后我完全不知道怎么往下写。毕竟这篇文章说的太好了。这篇文章分别在全局对象,函数,原型,方法中 this 具体的指向问题。堪称教科书级别的文章。但是我觉得还是有必要自己的方法去描述一下这个概念。
就像从零开始写一个 cpu 那样,我尝试使用一种增量模型来描述 this 这个概念。
-
“ this 指向当前对象”
我们姑且先这样记住,然后按照每个特出情况再对这句话进行一点修改,让最后的表述最接近真实的 this .(会不会有点小题大做了?)那么在浏览器宿主的全局环境下,this 所指向的对象就是对象 window
console.log(this === Window) // true
-
在函数中使用 this 的时候,不使用 new 关键字声明函数的时候 this 仍然指向 window,当使用 new 的时候指向这个函数。
//普通的声明 foo = "bar"; function xx(){ this.foo = "fuck"; } console.log(this.foo); //bar new xx(); console.log(this.foo); //bar console.log(new xx().foo) //fuck
是不是看的有点晕?为什么单独new 没变化,log出去就改变了this的值呢?我们得先知道new的时候编译器干了啥
英文好的读 这个 。下面的你就不用看了,英文不好的接着看。下面是不负责任的翻译:
new干了5件事
- 创造一个对象
- 把构造函数的 prototype 拷贝到 实例对象的 _proto_ 中。
- 让 this 这个变量指向新创建的对象
- 使用新创见的对象执行构造函数
- 最后返回新创建的对象
下面是两个例子
ObjMaker = function() {this.a = 'first';}; // 我叫 ObjMaker 是一个普通的构造函数 ObjMaker.prototype.b = 'second'; // 和所有的函数一样,我有很多可以改变的原型属性(prototype property),现在我被增加了一个叫 b 的属性。 // 与此同时,我还是一个对象.那么作为一个对象,我也会有很多不可访问的内部属性([[prototype]] property) obj1 = new ObjMaker(); // 现在我作为一个构造函数构造了一个叫obj1的对象 // 发生了三件事 // 1. 一个全新的叫obj1的空对象背创建了。类似于这样 obj1 = {} // 2. obj1 的[[prototype]] 属性被设置为ObjMaker.prototype(如果ObjMaker.prototype在此之后又作为构造 函数生成了一个新对象,obj1 的 [[prototype]] 不会改变但是你可以通过修改ObjMaker.porototype来添加原型) // 3. 执行这个函数
附上new的执行代码
function New(func) { var res = {}; if (func.prototype !== null) { res.__proto__ = func.prototype; } var ret = func.apply(res, Array.prototype.slice.call(arguments, 1)); if ((typeof ret === "object" || typeof ret === "function") && ret !== null) { return ret; } return res; }
上面虽然 new 了一个新的函数,但是这个构造函数并没有指向找到合适的对象返回出去,而在调用console.log的时候相当于生成了一个新的对象,然后把这个没有名字的新生成的对象打印出去了。
说到这,基本上new的东西就讲完了,如果想有更近一步的了解,你可以去看一下廖雪峰的教程 。(我还是觉得了廖雪峰的教程不是特别适合完全的新手看。)new 就先说到这,我们接着看this相关的内容
-
在原型中指向当前对象,但是在原型链中原型链底层函数中对this的操作会覆盖上层的值
function Thing1() { } Thing1.prototype.foo = "bar"; function Thing2() { this.foo = "foo"; } Thing2.prototype = new Thing1(); function Thing3() { } Thing3.prototype = new Thing2(); var thing = new Thing3(); console.log(thing.foo); //logs "foo" //原型链 thing -> Thing3 -> Thing2 -> Thing1 //每一次new 操作都会让 this 跳转一次,当编译器在查找方法或者属性的时候会依次向上寻找, //当编译器在thing2这层找到foo属性的时候就停止查找,这样就屏蔽了Thing1这一层的foo属性
实际上上面这个例子也是 JS 在模拟经典的对象继承
apply与call
终于讲到 apply 了。其实这两个在有了上面的基础以后,一句话就能说清楚。apply 和 call 就是用于改变函数内部 this 的指向。当某个对象,比如A对象有 a 方法,B对象有 b 方法,那么如果想让A对象也具有b方法,那么使用apply或者call就可以完成这样一个操作。
call和apply都是Function.prototype 下面的一个方法,所有的函数都具备这两种方法,这两种方法都具有相同的效果,唯一的区别在于调用方式不同。我们用一个简单的例子来说明问题。
function ghost(){};//我们定义一个鬼兵
ghost.prototype = {
EMP : function(){
//emp code here
},
HasSnipe : 0,
Say : function(){
console.log("Waiting on you");
}
} //这是一个升级了EMP但是没有升级狙击,有一句台词的的鬼兵。
var nova = new ghost;//诺娃是一个光荣的帝国鬼兵,具有鬼兵的技能。
var Scott = {};//Scott是一个新兵蛋子,他刚接受了帝国鬼兵的训练,可以使用技能,但是系统并没有为他分配武器。
//然而在侦查到一个小队满能量的哨兵,于是诺娃的武器给Scott了,
//命令Scott去使用EMP去清空那些哨兵的能量与护盾。
//在系统层面,我们这么做
Scott.EMP.call(nova);
然后诺娃掏出了她的大吊大刀把那些烧饼干掉了,就是这样。
实际上在这个例子已经很清楚了。更进一步的描述在于这里
下面抄一个正式一点的例子,为那些不玩SC的同学们准备的
function Man() {}
Man.prototype = {
valet: false,
wakeUp: function(event) {
console.log(this.valet + "? Some breakfase, please.");
}
};
//get "undefined? Some breakfast, please
var button = document.getElementById('morning');
button.addEventListener(
"click",
wooster.wakeUp,
false
);
//使用apply来改变 wakeUp 的上下文环境,即 wakeUp 中的this
var button = document.getElementById('morning2');
button.addEventListener(
"click",
function() {
Man.prototype.wakeUp.apply(wooster, arguments);
},
false
);
扩展阅读
http://www.cnblogs.com/TomXu/archive/2012/01/05/2305453.html
https://segmentfault.com/a/1190000002634958
https://developer.mozilla.org/en/docs/Web/JavaScript/Inheritance_and_the_prototype_chain
http://bonsaiden.github.io/JavaScript-Garden/zh/#object.prototype