聊一聊对象、原型和继承

730 阅读5分钟

基于原型的语言

在MDN对象原型一章有这样一段话:JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。本文从这段话为出发点聊一聊对象、原型和继承。

对象

什么是对象,怎么区分?

在JavaScript的数据类型中除了基础类型(Number、String、Boolean、Undefined、Null、Symbol、BigInt)就是引用类型(Object)也就是我们所说的对象。主要区别是基础类型都是不可变的,但对象不是具体的值而是指内存中的可以被标识符引用的一块区域因此可以被改变

对象的分类

为了便于理解对象和原型,本文中将对象分为如下三类(对象的分类从不同角度有很多种划分比如MDN的内置对象又或是W3C的本地对象、内置对象和宿主对象都是站在不同角度去理解对象)

  • 标准对象(有__proto__但是没有prototype属性的对象)
    • 普通对象
    • 原型对象 (只存在于函数对象,指构造函数的prototype对象,有construtor属性)
  • 函数对象 (由function创建,同样有__proto__,包含有prototype属性)

原型

什么是原型?

遵循ECMAScript标准,someObject.[[Prototype]] 符号是用于指向 someObject 的原型。简单理解也就是我们平常在浏览器中打印对象会看到的__proto__属性。

原型作为对象的内部属性是不能直接被访问的,但浏览器厂商为我们提供了__proto__这个非标准的访问器可以进行访问。在ES6中__proto__也被标准化为传统功能,以确保Web浏览器的兼容性。我们也可以使用ES5规定的标准Object.getPrototypeOf()Object.setPrototypeOf()来进行访问和设置。原型不应该与构造函数 func 的 prototype 属性相混淆。被构造函数创建的实例对象的 [[Prototype]] 指向 func 的 prototype 属性。Object.prototype 属性表示 Object 的原型对象。

原型的作用?

主要是为了JavaScript继承的实现,在构造函数中包含一个对象prototype,所有实例对象需要共享的属性和方法,都放在这个对象里面;那些不需要共享的属性和方法,就放在构造函数里面。实例对象一旦创建,将自动引用prototype对象的属性和方法来实现继承。

原型怎么生成的?

当构造器创建一个对象,为了解决对象的属性引用,该对象会隐式引用构造器的“prototype”属性(通过程序表达式 constructor.prototype 可以引用到构造器的“prototype”属性),并且添加到原型对象(prototype)里的属性,会通过继承与所有共享此原型的对象共享。 举个例子:

Person构造函数创建了名为p的实例对象,p对象的原型[[Prototype]]被创建并指向了它的构造函数Person的原型对象Person.prototype。当p.say()被执行时会现在p对象的属性中查找say方法,如果没有找到则会通过p的原型查找,如果还没找到会继续查找该对象原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或者达到链条的末尾(null)。最终在Person的原型对象(Person.prototype)中找到了say方法并执行,这种关系被称为原型链

new操作符

那么在构造函数生成实例的时候new操作符到底做了什么呢?

  1. 创建一个空的简单JavaScript对象;
  2. 链接该对象(即设置该对象的构造函数)到另一个对象 ;
  3. 将步骤1新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this。
<!--简单的模拟-->
function newFn(constructor){
  if(typeof constructor!=='function'){
    throw new TypeError('constructor is not a function');
  }
  let obj = new Object();
  Object.setPrototypeOf(obj, constructor.prototype);
  const res = constructor.apply(obj, Array.prototype.slice.call(arguments, 1));
  return typeof res === 'object' ? res : obj
}

继承

因为JavaScript不是传统面向对象的语言,网景公司在设计初期只是想设计一种网页脚本语言,使得浏览器可以与网页互动,因此他没有像JAVA、C++概念中的类。但在JavaScript中除了基本类型都是对象,需要一种机制来将对象联系起来,由此引入了继承了概念。 在经典面向对象的语言中继承的本质是扩展一个已有的Class,并生成新的Subclass,而在JavaScript中则是通过原型的方式来实现继承。

原型继承的实现

上文中实例对象p通过[[Prototype]]可以访问到构造函数原型对象中的属性和方法, 同理函数对象间也可以通过原型的机制实现继承。 JavaScript原型继承方式:

  1. 创建新的构造函数后在内部调用父构造函数并绑定this
  2. 将需要继承函数的原型对象绑定到新构造函数的原型上并设置正确的构造函数引用(使用setPrototypeOf的好处之一是不用重新设置constructor的引用)

上文中我们创建了一个Person的构造函数并带有name的属性和say的原型方法,现在我们定义一个Teacher的函数来继承Person的属性以及原型上的公共方法。

function Teacher(name, school){
    Person.call(this, name);
    this.school = school
}

Object.setPrototypeOf(Teacher.prototype, Person.prototype)

Teacher.prototype.getSchool = function(){
  console.log(`my school is ${this.school}`)
}

可以发现新的实例t通过[[Prototype]可以访问到Teacher以及Person构造函数原型对象中的属性和方法。

class继承

上文中我们虽然实现了基于原型的继承但缺点也显而易见就是太麻烦了,好消息是在ES6中引入了新的关键字class来帮助快速的实现类,类中自带有constructor构造函数的声明,需要注意的是这个类仅仅是函数的语法糖,本质上还是基于原型的继承。关于类的详细说明可以参考MDN中类元素的介绍。 这里我们通过ES6的类来实现继承

class Teacher extends Person{
  constructor(name, school){
    super(name);
    this.school = school;
  }

  getSchool(){
    console.log(`my school is ${this.school}`)
  }

}

总结

通过原型继承的机制让我们在创建一系列拥有相似特性的对象时可以非常简单的创建一个含有公共功能的通用对象,然后在更特殊对象类型中继承这些特性,并可以动态的扩展这些特性,这大概就是原型继承的魅力所在吧。

参考

developer.mozilla.org/zh-CN/docs/… www.ruanyifeng.com/blog/2011/0… www.liaoxuefeng.com/wiki/102291…