JavaScript中的原型(prototype)与继承

在JavaScript中,原型是用来模仿其他「类」语言继承机制的基础。原型并不复杂,原型只是一个对象。

一、原型对象

1.1 什么是原型对象

每当我们创建了一个函数后,这个函数就拥有了一个prototype属性,这个属性指向一个对象——原型对象。原型对象一开始就有一个constructor属性,指向我们创建的函数。而当我们对这个函数进行构造函数调用创建了一个实例对象,这个对象将会有一个特殊的内置属性[[prototype]],这个属性就是对函数原型对象的引用。构造函数、实例对象和原型对象的关系请看下图:

Person为「构造函数」,person1与person2为实例对象、Person Prototype为函数的原型对象。通过同一个函数构造的对象内部的[[prototype]]属性都指向了同一个原型对象,这是一种共有的关联关系,原型中的所有属性和方法都被这些实例共享。

从图上也看出了,实例与构造函数之间不是直接关联的,而是通过原型的constructor间接关联的。

无论是通过构造函数实例化的对象还是通过对象字面量创建的对象,他们都有对应的原型,对于用对象字面量创建的对象来说,其默认的原型为Object.prototype。实际上,几乎所有对象都有原型。

var Person = function(name){
    this.name = name;
}

var person1 = new Person('person1');
var obj1 = { name: 'obj1' };

console.log(Object.getPrototypeOf(obj1) === Object.prototype);//true
console.log(Object.getPrototypeOf(person1) === Person.prototype);//true

复制代码

1.2 原型链

既然我们说了所有对象都有原型,那么就意味着原型也有原型,即原型对象的[[prototype]]属性也指向另一个对象。如此一来就形成了一个原型链,而这个链的尽头是Object.prototype。

通过上图的[[prototype]]属性的指向可以清晰地看到原型对象之间的连接。 ## 1.2 原型对象的作用 原型对象有什么用呢?大家一定还记得引擎是如何在作用域链中查找一个变量的,作用域链用于查找声明的变量,而对象的属性则在对象及其原型链中查找。当读取对象的属性时,首先在对象中查找,当查找不到时开始从对象的原型中查找。根据之前提到的原型也是一个对象,所以如果在对象的原型中找不到,那么将会到原型的原型中找,直到找到或者到了原型链的尾端返回undefined为止。
var Person = function(name){
    this.name = name;
}

Person.prototype = {
	constructor:Person,
	age:23
}
console.log(person1.age);//23--原型中的属性
复制代码

1.2.1 属性屏蔽与设置

假设我们查找myObject.foo属性,而原型链上有多个name属性,会发生什么呢?与作用域链类似,会发生「屏蔽」。根据查找的机制,总会返回第一个找到的同名属性而忽略后续的同名属性。

那么给对象设置属性的时候呢?这时候情况就有点复杂了:

  1. 如果在[[Prototype]]链上层存在名为foo的普通数据访问属性并且没 有被标记为只读(writable:false),那就会直接在myObject中添加一个名为foo的新 属性,它是屏蔽属性。
  2. 如果在[[Prototype]]链上层存在foo,但是它被标记为只读(writable:false),那么 无法修改已有属性或者在myObject上创建屏蔽属性。如果运行在严格模式下,代码会 抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
  3. 如果在[[Prototype]]链上层存在foo并且它是一个setter(参见第3章),那就一定会 调用这个setter。foo不会被添加到(或者说屏蔽于)myObject,也不会重新定义foo这 个setter。 4.如果对象以及[[prototype]]链中都没有foo属性,foo属性将会被添加到myObject上。

简单地说分为四种情况:①有且可写 ②有但只读 ③有且是setter ④没有。只有①和④会在myObject上创建属性。当原型链上有同名只读属性时将会阻止同名属性的新增,如果是setter将会按照setter的逻辑操作。

var Person = function(name){
    this.name = name;
}
//修改原型
//Person.prototype.name = 'modifined';

//重新连接(替换)原型而不是修改原型
Person.prototype = {
    constructor:Person,//恢复constructor属性,但变成了可枚举
    name:'constructor'//默认是可写的
}
Object.defineProperty(Person.prototype,'age',{
    value:23,
    writable:false//设置为只读
})
var person1 = new Person('person1');
console.log(person1.age);//23

person1.name = 'tom';
person1.age = 20;//严格模式下将会报错

console.log(person1.age);//23——修改被忽略
console.log(person1.name);//tom
复制代码

在上面的代码中我们替换了Person的原型,注意这个操作将会导致实例失去与原原型的连接,转而关联替换后的原型。也就是说,实例再也无法访问原原型中的属性和方法。替换原型用于模仿继承机制,接下来将会讲到实际上并不是继承。

二、类和构造函数

2.1 没有「类」

JavaScript中并没有类似于Java的那种类,关于「类」的一切都是围绕着函数和原型来展开的。虽然ES6中增加了class特性,但是底层上它还是在操作函数和原型,也算不上是真正的类。我们看看Java的继承和JavaScript的继承:

//java
public class Foo{
	String name = "foo";
	public String getName(){
		System.out.println(this.name);
	}
}
public class Bar extends Foo{

}
Bar bar1 = new Bar();
Bar bar2 = new Bar();
Foo foo = new Foo();

foo.name = 'modifined';
bar1.name = "bar1";
bar2.getName();//foo--丝毫不受影响
复制代码
//js
var Foo = function Foo(){

}
Foo.prototype = {
	name:'foo',
	getName:function getName(){
		console.log(this.name);
	}
}

var Bar = function(){

}
Bar.prototype = Foo.prototype;//"继承"Foo
Bar.prototype.sayHi = function sayHi(){//拓展Foo
	console.log('hi!');
}
var bar1 = new Bar();
var bar2 = new Bar();
Foo.prototype.name = 'modifined';

bar1.getName();//'modifined'
bar2.getName();//'modifined'
复制代码

从这两段代码可以看到,在Java中继承意味着子类实例对属性进行了私有化,无论是父类对象还是其他子类对象都无法影响到继承的属性。而在JavaScript中,"子类实例"只是关联了共同的对象,只要那个对象更改了就能马上反映到每一个"子类对象"。所以我觉得不应该用「继承」来形容这种机制了,用「委托」更形象,它们本质上是对象之间的关联。

2.2 没有「构造函数」

JavaScript中也没有构造函数。所有的函数都是一样的,都可以通过函数名+()调用,所有的函数都可以new实例化得到一个对象。如果你认为可以new的就是构造函数,那么JavaScript中所有的函数都是构造函数了。

var foo = function foo(){
	console.log('foo');
}
var obj = new foo();//'foo'
console.log(obj.constructor === foo);//true

foo.prototype.constructor = Object;
console.log(obj.constructor === Object);//true
复制代码

我们可以看到,一个普通的foo函数也可以进行new调用实例化一个对象,可能你还想通过constructor属性去证明他是obj的构造函数。但是可以看到我们随后修改了原型中的constructor属性,obj的"构造函数"也发生改变了。这说明什么?constructor根本就没那么「权威」,它只是原型中的一个属性,默认指向了被构造调用的函数,我们可以自行修改它。

2.3 还是想要「类」

可能有时候写「类」语言写多了一下子还没转过来,还是想去模仿类,怎么办?没关系有办法的——方法可以共用,属性要私有嘛,请看代码:

var father = function father(name,age){
	this.name = name;
	this.age = age;
}
father.prototype = {
	constructor:father,//不让这个属性丢失
	sayName:function sayName(){
		console.log(this.name);
	},
	sayAge:function sayAge(){
		console.log(this.age);
	}
}
var son = function(name,age){
	father.call(this,name,age);//构造函数借用,绑定son的this
}
son.prototype = Object.create(father.prototype);//新建对象,与原father.prototype隔离
//不要这样:son.prototype = father.prototype;
//否则后续对son.prototype的拓展都是在修改father.prototype

var son1 = new son('tom',12);
var son2 = new son('jack',15);
father.prototype.name = 'mike';

son1.sayName();//tom
son1.name = 'kobee';//屏蔽
son2.sayName();//jack
复制代码

这里主要有两点:一是构造函数借调、二是隔离原父函数的prototype。借调使得我们可以"借用"父函数的代码,当然我们要绑定this才能实现对新对象赋值;接着我们要隔离父函数的原型,以免我们拓展子函数原型时影响到父函数原型。通过Object.create(...)创建一个空的新对象,这个对象的[[prototype]]指向我们传递的对象,这里是father.prototype。我们对son.prototype的修改都是在修改这个新对象,而不是father.prototype。

好了,类模仿完毕了,不过我觉得如果用类的思维去理解这段代码效率会比较低,如果用函数+原型的概念去理解会更好,你觉得呢?

2.4 寻找原型(委托)对象

那么怎么寻找一个对象的原型呢。如果还是用类的思维去判断的话,要判断一个对象是否是一个"类"的实例就会这么做:

var foo = function foo(){};
var obj = new foo();

obj instanceof foo;//true
复制代码

instanceof操作符的左操作数是一个对象,右操作数是一个函数,实际上它在问:在obj的[[prototype]]链中有没有foo.prototype?在这里明显有~所以我们知道了obj的原型(委托)对象为foo.prototype

但是如果想判断两个对象是否关通过[[prototype]]关联呢?instanceof肯定不行,它的右操作数是函数,而我们只有两个对象。如果你已经理解了JavaScript中对象关联(委托)的这个思想,可以这么做:

var foo = function foo(){};
var a = new foo();
var b = Object.create(o1);

function isRelatedTo(o1, o2) {
	function F(){}
	F.prototype = o2;
	return o1 instanceof F;
}

console.log(isRelatedTo(b,a));//true
复制代码

说实话这样做没有错,o2被关联到F函数的原型上,然后用instanceof操作符就可判断o2是否在o1的[[prototype]]链上。这里对象a的确在对象b的原型链上。

但是我们有必要绕一个大圈吗?或者是说没有直接判断的工具吗?有的:

var foo = function foo(){};
var a = new foo();

console.log(foo.prototype.isPrototypeOf(a));//true

复制代码

a.isPrototypeOf(b)在问:a是否出现在b的[[prototype]]链上?这里与上面不同的是,我们用两个对象进行判断,即进行了原型与对象之间的关联判断。而不是"类"与对象之间关联的判断,

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值