原型继承基础知识总结

  这段时间把原型的相关内容又详细过了一遍,收获还是不少,现将一些知识点记录下,供大家参考学习,也为自己查漏补缺。内容可能有些混乱,没有列一个清晰的大纲,就跟着笔记来吧。

1. constructor和_proto_基础

  首先明确一点,constructor属性是存在于函数的prototype对象上的,且是自有属性,而proto(下划线好像被markdown转义掉了)属性是存在于每个对象中的,且是继承属性
  看例子:

var ppp = function(){alert(1)};
console.log(ppp.prototype.hasOwnProperty('constructor'))//true
console.log(ppp.prototype.hasOwnProperty('__proto__'))//false

注意:hasOwnProperty()方法的入参一定带上引号,我就吃过这个亏…

再来看:

console.log(Object.prototype.hasOwnProperty('__proto__'))//true

  这说明了啥?constructor属性没毛病,就是prototype上的自有属性。而proto就不是自有属性了,是原型链上的属性,源头是在Object.prototype对象上的(而且这个属性只存在与原型上,无法成为自有属性)。
再来看个例子:
这里写图片描述
这说明了啥?赋值操作修改的也是原型链上的proto属性,而没有生成一个自有属性proto
注意proto属性不应该被显式使用,本文只是为了说明问题,这种做法并不提倡。如果确实要获取这个属性,请使用Object.getPrototypeOf()。

  这里还有一个constructor属性的妙用例子,可以看看。

2.for…in/hasOwnProperty/propertyIsEnumerable

  上面我们使用了hasOwnProperty来判断属性是否是自有属性。而对于in运算符,如果对象的自有属性继承属性中包含这个属性,则返回true。propertyIsEnumerable()是hasOwnProperty()的增强版,只有检测到是自有属性且这个属性的可枚举性为true时才能返回true。
  可以看到,只有for…in能访问到原型上的属性(可枚举)。

3.关于属性的设置和屏蔽操作

  上面提到一个概念——可枚举性,这是属性描述符中的一个,还有writable、configurable、getter、setter。有同学问了,这篇文章不是讲原型、继承的么,为什么会说到属性的设置和屏蔽呢?不冲突,这两者有密切的联系。

给一个对象设置属性并不仅仅是添加一个新属性或者修改已有的属性值。现在我们完整地讲解一下这个过程:

如果 myObject 对象中包含名为 foo 的普通数据访问属性, 这条赋值语句只会修改已有的属性值。

如果 foo 不是直接存在于 myObject 中, [[Prototype]] 链就会被遍历, 类似 [[Get]] 操作。 如果原型链上找不到 foo, foo 就会被直接添加到 myObject 上。然而, 如果 foo 存在于原型链上层, 赋值语句myObject.foo = “bar” 的行为就会有些不同 (而且可能很出人意料)。 稍后我们会进行介绍。

如果属性名 foo 既出现在 myObject 中也出现在 myObject 的 [[Prototype]] 链上层, 那么就会发生屏蔽。 myObject 中包含的 foo 属性会屏蔽原型链上层的所有 foo 属性, 因为myObject.foo 总是会选择原型链中最底层的foo 属性。

屏蔽比我们想象中更加复杂。 下面我们分析一下如果 foo 不直接存在于 myObject 中而是存在于原型链上层时 myObject.foo = “bar” 会出现的三种情况。

  1. 如果在 [[Prototype]] 链上层存在名为 foo 的普通数据访问属性并且没有被标记为只读(writable:false), 那就会直接在 myObject 中添加一个名为 foo 的新属性, 它是屏蔽属性。

  2. 如果在 [[Prototype]] 链上层存在 foo, 但是它被标记为只读(writable:false), 那么无法修改已有属性或者在 myObject 上创建屏蔽属性。 如果运行在严格模式下, 代码会抛出一个错误。 否则, 这条赋值语句会被忽略。 总之, 不会发生屏蔽。

  3. 如果在 [[Prototype]] 链上层存在 foo 并且它是一个 setter(参见第 3 章), 那就一定会调用这个 setter。 foo 不会被添加到(或者说屏蔽于) myObject, 也不会重新定义 foo 这个 setter。

大多数开发者都认为如果向 [[Prototype]] 链上层已经存在的属性([[Put]]) 赋值, 就一 定会触发屏蔽, 但是如你所见,三种情况中只有一种(第一种) 是这样的。 如果你希望在第二种和第三种情况下也屏蔽 foo, 那就不能使用 = 操作符来赋值, 而是使用Object.defineProperty(..)(参见第 3 章) 来向 myObject 添加 foo。

  这是摘自小黄书上的一段内容,讲得非常仔细,非常清楚,我就不啰嗦了。

4.new操作符/Object.create()/Object.setPrototypeOf()

  这三种方法都是设置原型关系的方法,之前可能区分得不是很清楚,现在我们好好理一下。
先看new操作符的操作:
1. var obj={}; 也就是说,初始化一个对象obj。
2.obj._proto_=a.prototype;
3. a.call(obj);也就是说构造obj,也可以称之为初始化obj。
关键记住2,3两点:1.设置了原型关系。2.调用了构造函数,并且this指向初始化的对象。

再来看Object.create():
核心代码:

function create(o){ 
    function F(){}; 
    F.prototype = o; 
    return new F(); 
}

  首先注意到这个方法是用一个对象来“生成”另一个对象,是对象和对象之间的关系,而new操作符是对象和函数之间的关系。看代码:

var Base = function () {
    this.a = 2
}
Base.prototype.a = 3;
Base.a = 4
var o1 = new Base();
var o2 = Object.create(Base);//o2.__proto__ = Base
console.log(o1.a);//2(3被2屏蔽了)
console.log(o2.a);//4
console.log(o2.a === Base.a);//千万不要以为o2.a === Base.prototype.a

  这样看就很清晰了吧。

最后来看Object.setPrototypeOf():
  为现有对象设置原型,返回一个新对象。接收两个参数:第一个是现有对象,第二是原型对象。

首先看到这个方法也是操作两个对象之间的关系,上代码:

function Person() {
    this.name = '1'
}
Person.prototype.get1 = function(){console.log(111)}
var p = {};
//等同于将构造函数的原型对象赋给实例对象p的属性__proto__
Object.setPrototypeOf(p,Person.prototype);
p.get1()//111

  可以看到本质上仍然是p.proto = Person.prototype,这和Object.create()是一样的。那么它们之间有什么区别呢?

Object.create和Object.setPrototypeOf的区别:前者是需要抛弃原来的对象(垃圾回收),然后创建一个新的对象,后者是直接在目标对象上修改。所以setPrototypeOf方法效率可能更高。

5.instanceof/isPrototypeOf/Object.getPrototypeOf

直接上代码:

var a = {};
var b = Object.create(a);
var F = function(){};
var f = new F();
console.log(b.__proto__ === a) //true
console.log(a.isPrototypeOf(b)) //true
console.log(Object.getPrototypeOf(b)) //{}
console.log(f instanceof F) //true
console.log(b instanceof a)//报错Uncaught TypeError: Right-hand side of 'instanceof' is not callable

  可以看到instanceof和new操作符对应,表示对象和函数的关系;isPrototypeOf和Object.create/Object.setPrototypeOf对应,表示对象和对象之间的关系。而Object.getPrototypeOf(b)就是获取b.proto,到这里可以看到ECMA为proto找的代替方法为Object.getPrototypeOf()和Object.setPrototypeOf(),分别为get和set。

6.几个例子

  我们来看几个例子,检验下是否完全掌握了上述内容。

  • 例子1
function Mammal() {
  this.isMammal = 'yes';
}
function MammalSpecies(sMammalSpecies) {
  this.species = sMammalSpecies;
}
MammalSpecies.prototype = new Mammal();
console.log(MammalSpecies.prototype.constructor)

分析:
  如果没有MammalSpecies.prototype = new Mammal();这句话很明显MammalSpecies.prototype.constructor是指向MammalSpecies的。但是这句话重写了MammalSpecies.prototype,所以constructor属性也不存在了。根据上面讲到的new操作符的内容,这句话的实质是MammalSpecies.prototype.proto = Mammal.prototype,所以constructor顺着原型链网上找,变成了Mammal.prototype.constructor,很明显,这就是Mammal()函数了,所以输出:

ƒ Mammal() {
  this.isMammal = 'yes';
}
  • 例子2
function Mammal() {
  this.isMammal = 'yes';
}
function MammalSpecies(sMammalSpecies) {
  this.species = sMammalSpecies;
}
MammalSpecies.prototype.__proto__ = Mammal.prototype;
console.log(MammalSpecies.prototype.constructor)

分析:
  同样,如果没有MammalSpecies.prototype.proto = Mammal.prototype;这句话,MammalSpecies.prototype.constructor的值是MammalSpecies函数。MammalSpecies.prototype上有两个属性,constructor和proto,仔细看这句话,只是重写了prototype上的proto,而不是上例中的prototype,所以对于MammalSpecies.prototype.constructor没有影响,还是指向MammalSpecies。所以输出:

ƒ MammalSpecies(sMammalSpecies) {
  this.species = sMammalSpecies;
}
  • 例子3
function Mammal() {
  this.isMammal = 'yes';
}
function MammalSpecies(sMammalSpecies) {
  this.species = sMammalSpecies;
}
MammalSpecies.prototype = Object.create(Mammal.prototype)
console.log(MammalSpecies.prototype.constructor)

分析:
  本例中MammalSpecies.prototype被重写,所以MammalSpecies.prototype上的constructor属性要从原型链上寻找了,根据上文中对于Object.create的解读,可以得出,结果就是Mammal.prototype.constructor,也就是Mammal函数。如下是输出:

ƒ Mammal() {
  this.isMammal = 'yes';
}
  • 例子4
function Mammal() {
  this.isMammal = 'yes';
}
function MammalSpecies(sMammalSpecies) {
  this.species = sMammalSpecies;
}
Object.setPrototypeOf(MammalSpecies.prototype,Mammal.prototype)
console.log(MammalSpecies.prototype.constructor)

分析:
  根据setPrototypeOf的解读可知这个方法就是用来设置proto属性的,所以和第二个例子一致。输出:

ƒ MammalSpecies(sMammalSpecies) {
  this.species = sMammalSpecies;
}

7.继承

  终于说到继承了,这里总结4种经典继承方式。这段内容主要来源于红宝书,我这里做个简单总结,大家有疑问或者想看详细教程,请翻阅红宝书。

  • 原型继承
      其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
function SuperType(){
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
};
function SubType(){
    this.subproperty = false;
}
//继承了 SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){
    return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true

  原型链继承的问题

问题一:
  当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;
问题二:
  在创建子类型(例如创建Son的实例)时,不能向超类型(例如Father)的构造函数中传递参数.

  问题2是显然的,我们来分析下问题1。SubType.prototype = new SuperType();执行完毕,SubType.prototype.property被赋值为true,instance.getSuperValue()通过原型链查找到SubType.prototype.getSuperValue()继续——SuperType.prototype.getSuperValue()。函数体中的this指向instance,而instance中没有property这个自有属性,又沿着原型链寻找,找到SubType.prototype.property,值为true。可以看到,该属性被所有实例所共享。

  • 构造函数继承
      这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。
function SuperType(){
    this.colors = ["red", "blue", "green"];
}
function SubType(){
    //继承了 SuperType
    SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green"

  很显然,构造函数继承解决了上述原型链继承的两个问题,但是其本身又有新的问题:

方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。

  • 组合继承
      组合继承(combination inheritance),有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。
function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
    alert(this.name);
};
function SubType(name, age){
    //继承属性
    SuperType.call(this, name);
    this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    alert(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

  可以看到组合式继承解决了构造函数继承和原型链继承的问题,但是,它也有自己的不足:

组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。

  • 寄生式组合继承
      所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
    alert(this.name);
};
function SubType(name, age){
    SuperType.call(this, name);
    this.age = age;
}
SubType.prototype = Object.create(SuperType.prototype)
SubType.prototype.sayAge = function(){
    alert(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

  这个例子的高效率体现在它只调用了一次 SuperType 构造函数,并且因此避免了在SubType.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用instanceof 和isPrototypeOf()。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

8.小结

  以上就是原型继承基础总结的全部内容,如果后续有需要补充的我再更新。发现有问题的部分希望大家指正!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JavaScript 原型链是 JavaScript 对象模型中的一个重要概念。每个 JavaScript 对象都有一个原型对象,而这个原型对象又有自己的原型对象,形成了一个链式结构,即原型链。 在 JavaScript 中,每个对象都有一个 `__proto__` 属性,指向它的原型对象。当我们访问一个对象的属性时,如果该对象本身没有这个属性,就会去它的原型对象上查找,如果还没有,就会继续查找原型对象的原型对象,直到找到该属性或者到达原型链的末尾。 当我们创建一个对象时,JavaScript 引擎会自动为该对象添加一个 `__proto__` 属性,指向该对象的构造函数的原型对象。例如,我们可以通过如下方式创建一个对象: ```javascript function Person(name, age) { this.name = name; this.age = age; } var person = new Person('Tom', 18); ``` 在上面的代码中,`Person` 是一个构造函数,`person` 是一个通过 `Person` 构造函数创建的对象。在 `person` 对象中,它的 `__proto__` 属性指向 `Person.prototype`,而 `Person.prototype` 的 `__proto__` 属性又指向 `Object.prototype`,最终形成了一个原型链。 原型链的作用是实现对象之间的继承。当我们访问一个对象的属性时,如果该对象本身没有这个属性,就会去它的原型对象上查找,这就实现了属性的继承。 同时,原型链还可以实现方法的继承。我们可以将一些公共的方法定义在父对象的原型对象上,这样子对象就可以继承这些方法,从而减少代码的重复。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值