JavaScript深入之从原型、原型链到继承

1,820 阅读7分钟

原型、原型链

在我们正式开讲之前,大家都应该明白两个定义,一个是原型、一个是原型链:

原型:所有的函数默认都会拥有一个名为prototype的公有并且不可枚举的属性,他会指向另一个对象,这个对象就是函数的原型

原型链:每一个原型对象都有一个原型链指针__proto__,该指针是指向上一层的原型对象,而上一层的原型对象的结构依然类似,一直往上查找直到Object.prototype.__ proto__=null表示到达原型链顶端,整个流程被称为原型链。

大家可以把他想象成梯子,一节一节的通过__proto__连在一起。查找对象属性和方法的时候会一直向上查找,直到梯子的顶端。其实在js里一切皆是对象,不是java类语言,没有复制机制,对象之间联系只能通过原型链做为关联。

原型 prototype 和 __ proto__

从下面代码中我们可以看到Person函数有prototype属性,在控制台打印这个属性会返回的是一个对象,这个对象就是我们所说的原型。原型里有constructor和__proto__属性,new实列化一个对象person, person有一个属性叫__proto__,这个属性会指向该对象的原型。

function Person() {}
var person = new Person() 
console.log(person.__proto__ === Person.prototype) 
console.log(Person === Person.prototype.constructor)
  • 每个对象都有一个__ proto__属性,并且指向它的 prototype 原型对象
  • 每个构造函数都有prototype属性,prototype原型对象里的constructor指向构造函数本身
  • 实例对象的__proto__指向原型对象

原型链:

var obj = { a: 2}
var myObj = Object.create(obj)
myObj.a // 2

如果无法在对象本身找到需要的属性,就会继续访问对象的原型链,myObj上并没有a属性,因此通过原型链查找到obj上的a属性。如果obj中也找不到a,并且原型链不为空,就会继续向上查找,直到原型链顶端,还找不到才会返回undefined。(使用in操作符来检查属性在对象中是否存在是,同样会查找对象的整条原型链)

原型链:每一个原型对象都有一个原型链指针__proto__,该指针是指向上一层的原型对象,而上一层的原型对象的结构依然类似。当 Object.prototype.__ proto__ 的值为 null,说明到达了原型链的顶端。

var arr = [1,2,3]
arr.valueOf()  //  [1, 2, 3]

原型链如下:

arr ---> Array.prototype ---> Object.prototype ---> null

这就是传说中的原型链,层层向上查找,最后还没有就返回undefined

扩展:所有普通的原型链最终都会指向内置的Object.prototype,它包含.toString、.valueOf等许多通用的功能,因此普通的对象也都能能直接使用这些方法。

属性的设置和屏蔽

给一个对象设置属性并不仅仅是添加一个新属性或者修改已有的属性值。

var obj1 = {
  a: 1
}
var myObj = Object.create(obj1)
console.log(obj1.hasOwnProperty("a"))  // true  hasOwnProperty:判断一个属性是否是在对象上,不包括原型上的属性
console.log(myObj.hasOwnProperty("a"))  // false
myObj.a++
console.log(obj1, myObj)
console.log(myObj.hasOwnProperty("a"))  // true
  • 1、如果obj对象中包含名为foo的普通数据访问属性,这条赋值语句只会修改已有的属性值

  • 2、如果obj上没有foo属性,则会查找原型链,如果原型链查找不到foo,foo就会被直接添加到obj上

  • 3、如果foo没有在obj上,出现在原型链上,当foo没有标记为只读,会在obj上添加一个名为foo的新属性,它是屏蔽属性,当foo标记为只读,严格模式下会报错,否则会忽略

  • 4、如果属性foo即出现在obj中也出现在obj的原型链上层,obj中包含的属性会屏蔽原型链上层所有的foo属性,直接会在对象上添加一个新属性

对象关联

Object.create(...):会创建一个新对象并把它关联到指定的对象,可以充分利用原型链的机制并且避免不必要的麻烦(比如使用new的构造函数调用会生成.prototype和.constructor)

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

new(): 一个空对象,这个对象原型指向构造函数的prototype,执行构造函数后返回这个对象。如果不要父类的属性跟方法,在函数的prototype上去new这个父类。。

继承

继承是指一个对象直接使用另外一个对象的属性和方法

在JavaScript中,并没有复制机制,也不能创建一个类的多个实例,只能创建多个对象,然后在他们之间创建一个关联,这样一个对象就可以通过委托访问另一个对象的属性和方法,这个机制被称为原型链继承,更准确的可以描述为委托

function Animal(name) {
  this.name = name
  this.sleep = function() {
    console.log(this.name + '正在睡觉!')
  }
}
Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃' + food)
}

1、原型链继承

function Cat() {}
Cat.prototype = new Animal('cat')

var cat = new Cat()
console.log(cat.name)
console.log(cat.eat('fish'))
console.log(cat.sleep())

重点:让新实例的原型等于父类的实例。
特点:基于原型链,继承父类实例的属性和方法,也能继承原型上的属性和方法。
缺点:不能实现多继承。所有新实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改!)

2、构造继承

function Dog() {
  Animal.call(this, 'tom')
}
var dog = new Dog()
console.log(dog.name)
console.log(dog.sleep())
// console.log(dog.eat('bone'))  // 报错

重点:用.call()和.apply()将父类构造函数引入子类函数,自执行
特点:可以实现多继承。在子实例中可向父实例传参。
缺点:只能继承父类实例的属性和方法,不能继承原型上的属性和方法。每次用每次都要重新调用。

3、组合继承

function Snake() {
  Animal.call(this, 'snake')
}
Snake.prototype = new Animal()

var snake = new Snake()
console.log(snake.name)
console.log(snake.sleep())
console.log(snake.eat('apple'))

重点:构造继承和原型链继承的组合体。通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
缺点:调用了两次父类构造函数(耗内存),子类的构造函数会代替原型上的那个父类构造函数。

4、原型式继承

function content(obj) {
  function F(){}
  F.prototype = obj
  return new F()
}
var Irabbit = new Animal('rabbit')  // 拿到父类的实类
var rabbit = content(Irabbit)
console.log(rabbit.name)
console.log(rabbit.sleep())
console.log(rabbit.eat('turnip'))

重点:用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。object.create()就是这个原理。
特点:类似于复制一个对象,用函数来包装。
缺点:所有实例都会继承原型上的属性。无法实现复用。(新实例属性都是后面添加的)

5、寄生、寄生组合继承(常用)

寄生:在函数内返回对象然后调用
组合:1、函数的原型等于另一个实例。2、在函数中用apply或者call引入另一个构造函数,可传参

var con = content(Animal.prototype) // con实例(F实例)的原型继承了父类函数的原型

// 组合
function Pig() {
  Animal.call(this, 'pig')  // 继承了父类构造函数的属性
} // 解决类组合式两次调用构造函数属性的缺点。
Pig.prototype = con // 继承con的实例
con.constructor = Pig // 修复实例

var pig = new Pig()
console.log(pig.name)
console.log(pig.sleep())
console.log(pig.eat('rice'))

修复了组合继承的问题

6、拷贝继承、实例继承(记住名字就行,忽略)

总结

如果要访问对象中并不存在的一个属性,[[Get]]操作就会查找对象内部原型关联的对象,这个关联关系实际上定义了一条原型链,在查找属性和方法时会对它进行遍历。

所有普通对象都有内置的Object.prototype, 指向原型链顶端,如果在原型链中找不到指定的属性就会停止。toSting()、valueOf() 和其他一些通用功能都存在Object.prototype对象上,因此所有的对象都可以使用它们

继承可以分为7种,其实重要掌握上面例子中的5种就够了。