JS关于继承

Javascript是一门基于原型链的语言
构造函数,原型属性与实例对象三者的关系:

function Person(name, age) {
    var gender = girl // ①
    this.name = name // ②
    this.age = age
}

// ③
Person.prototype.sayName = function() { 
    alert(this.name) 
}

// ④
var kitty = new Person('kitty', 14)

kitty.sayName() // kitty
  • Person 是一个构造函数(它用来“构造”对象,并且是一个函数),①处gender是该构造函数的“私有属性”,②处的语句定义了该构造函数的“自有属性”;
  • ③处的 prototypePerson 的“原型对象”(它是实例对象的“原型”,同时它是一个对象,但同时它也是构造函数的“属性”,所以也有人称它为“原型属性”),该对象上定义的所有属性(和方法)都会被“实例对象”所“继承”;
  • ④处的变量“kitty”的值是构造函数 Person 的“实例对象”(它是由构造函数生成的一个实例,同时,它是一个对象),它可以访问到两种属性,一种是通过构造函数生成的“自有属性”,一种是原型对象可以访问的所有属性;

基本概念搞清楚之后,开始今天的主题 -- “继承”:
为什么我们的实例对象可以访问到构造函数原型属性上的属性?答案是因为 “每一个对象自身都拥有一个隐式的[[proto]]属性,该属性默认是一个指向其构造函数原型属性的指针,这个隐式的属性就是原型”。

当JavaScript引擎发现一个对象访问一个属性时,会默认调用一个内置的[[Get]]方法,其会首先查找对象的“自有属性”,如果没有找到则会在[[proto]]属性指向的原型属性中继续查找,如果还没有找到的话,你知道其实原型属性也是一个对象,所以它也有一个隐式的[[proto]]属性指向它的原型属性...,正如你所料,如果一直没有找到该属性,JavaScript引擎会一直这样找下去,直到找到最顶部构造函数Objectprototype原型属性,如果还是没有找到,会返回一个undefined值。这个不断查找的过程,有一个形象生动的名字“攀爬原型链”。
链的终点是Object.prototype对象,因此Object.prototype没有原型。
当我们构建一个对象,这个对象的默认的原型就是Object.prototype
在chrome中验证一下:

var a = {}
Object.prototype === a.__proto__  // true

在Chorme中,我们可以通过浏览器为对象添加的_proto_属性访问到[[proto]]的值。

两个重要概念:

  1. 每一个对象自身都拥有一个隐式的[[proto]]属性,该属性默认是一个指向其构造函数原型属性的指针;
  2. 几乎所有函数都拥有prototype原型属性;(Math等特殊对象除外)

在Javascript里实现继承:
即我们想要一个对象能访问另一个对象的属性,同时,这个对象还能够添加自己新的属性或是覆盖可访问的另一个对象的属性,我们实现这个目标的方式叫做“继承”。
实现继承的方式:

  1. 创建一个对象并指定其继承对象(原型对象);
  2. 修改构造函数的原型属性(对象);
    但是注意一点,对于一个已经定义的对象,我们无法再改变其继承关系,我们的第一种方式只能在“创建对象时”定义对象的继承对象。为什么呢?因为“我们设置一个对象的继承关系,本质上是在操作对象隐式的[[proto]]属性”,而Javascript只为我们开通了在对象创建时定义[[proto]]属性的权限,而拒绝让我们在对象定义时再修改或访问这一属性(所以它是“隐式”的)。

好了,是时候看看JavaScript世界中继承的主角了 -- Object.create()

  1. 关于 Object.create() 和对象继承
    Object.create() 函数是JavaScript提供给我们的一个在创建对象时设置对象内部[[proto]]属性的API,从而决定对象所继承的对象,从而以我们想要的方式实现继承。
    让我们细致的了解一下Object.create()函数:
var x = { 
    name: 'tom',
    sayName: function() {
        console.log(this.name)
    }
}
var y = Object.create(x, {
    name: {
        configurable: true,
        enumerable: true,
        value: 'kitty',
        writable: true, 
    }
})
y.sayName() // 'kitty'

Object.create()函数接收两个参数,第一个参数是创建对象想要继承的原型对象,第二个参数是一个属性描述对象,然后会返回一个对象。

让我们谈谈在调用Object.create()时究竟发生了什么:

  1. 创建了一个空对象,并赋值给相应变量;
  2. 将第一个参数对象设置为该对象[[proto]]属性的值;
  3. 在该对象上调用defineProperty()方法,并将第二个参数传入该方法中;
    (对比:new 一个对象的时候,发生了什么?
    1. 创建一个空对象,作为将要返回的函数实例。
    2. 将这个空对象的__proto__原型,指向构造函数的prototype属性。
    3. 将这个空对象赋值给函数内部的this关键字。
    4. 开始执行构造函数内部的代码。)
      用伪代码来表示:
function New(fn) {
    var tmp = {}
    tmp.__proto__ = fn.prototype
    fn.call(tmp)
    return tmp
}

Object.create()这样的方法有很多局限,比如我们只能在创建对象时设置对象的继承对象,又比如这种设置继承的方式是一次性的,我们永远无法依靠这种方式创造出多个有相同继承关系的对象,而对于这种情况,就要请出我们的第二个主角 -- prototype原型对象。

关于prototype 和构造函数继承
构造函数生产实例对象的过程本身就是一种天然的继承。实例对象天然的继承着原型对象的所有属性,这其实是Javascript提供给开发者第二种(也是默认的)设置对象 [[proto]] 属性的方法。
但是其缺陷也很明显:只存在两层继承。自定义构造函数的prototype对象继承Object构造函数的prototype属性,构造函数的实例对象继承构造函数的prototype属性。而我们有时候想要更灵活,满足需求,甚至是”更长“的原型链(或者说是”继承链“)。这是JavaScript默认的继承模式下无法实现的,但解决方式也很符合直觉,既然我们无法修改对象的[[proto]]属性,我们就去修改[[proto]]属性指向的对象 -- 原型对象。

function Foo(x, y) {
    this.x = x
    this.y = y
}
Foo.prototype.sayX = function() {
    console.log(this.x)
} 
Foo.prototype.sayY = function() {
    console.log(this.y)
}

function Bar(z) {
    this.z = z 
    this.x = 10
}
Bar.prototype = Object.create(Foo.prototype) // 注意这里
Bar.prototype.sayZ = function() {
    console.log(this.z)
}
Bar.prototype.constructor = Bar //Bar.prototype.constructor应该指向构造函数

var o = new Bar(1)
o.sayX() // 10
o.sayZ() // 1

通过修改了构造函数Bar的原型属性,将其值设置为一个继承对象为Foo.prototype空对象,在之后,又在该对象添加了一些属性和方法。这样,构造函数Bar的实例对象就会在查询属性时攀爬原型链,从自有属性开始,途径Bar.prototypeFoo.prototype,最终到达Object.prototype

这种继承的方式就是“构造函数继承”,是一种重要的继承方法。

但是有一个问题,如果我们在源代码上添加一条o.sayY()会发生什么?答案是控制台会输出undefined。这是因为构造函数Bar的实例对象不拥有构造函数Foo设置的自有属性。

如何让构造函数Bar的实例对象不拥有构造函数Foo设置的自有属性

function Foo(x, y) {
    this.x = x
    this.y = y
}
Foo.prototype.sayX = function() {
    console.log(this.x)
} 
Foo.prototype.sayY = function() {
    console.log(this.y)
}

function Bar(z) {
    this.z = z 
    this.x = 10
    Foo.call(this, z, z) // 注意这里
}
Bar.prototype = Object.create(Foo.prototype) 
Bar.prototype.sayZ = function() {
    console.log(this.z)
}
Bar.prototype.constructor = Bar

var o = new Bar(1)
o.sayX() // 1
o.sayY() // 1
o.sayZ() // 1

让我们以单纯的函数视角看待构造函数Foo,它不过是往this所指的对象上添加了两个属性,然后返回了undefined值,当我们单纯调用该函数时,this的指向为window(不明白为什么指向window,你可以阅读我的这篇文章)。但是通过call()apply()函数,我们可以人为的改变函数内this指针的指向,所以我们将构造函数内的this传入call()函数中,奇妙的事情发生了,原先为Foo函数实例对象添加的属性现在添加到了Bar函数的实例对象上!

new 和 “构造函数”
JavaScript中的函数很奇怪,函数的prototype是故意暴露出来的,而且这个属性还不为空,还有prototype还有另一个属性叫constructor,这个constructor竟然又引用回来了这个函数本身!

JS关于继承_第1张图片
prototype、constructor、函数.png

你可能感兴趣的:(JS关于继承)