JavaScript基于原型链和继承的类详解

原文地址:详解javascript的类

  Javascript从当初的一个“弹窗语言”,一步步发展成为现在前后端通吃的庞然大物。javascript的受欢迎程度也是与日俱增,今年最受欢迎编程语言又是花落javascript,这实在是一个充满了活力的语言。如今,随随便一个网页javascript代码量都不下数千行,要是node后端那更不止几千行了。(WTF)代码量的增加给编程带来的首要的问题就是如何去编写和维护如此庞大的代码呢?答案就是模块化思想,其实就是面向对象(OOP)编程,如今比较流行的三大前端框架(angular,react,vue)基本都实现了组件化编程,但组件化和我们所说的模块化又有所不同,应该可以这么理解,组件化是模块化的升级版,模块化是组件化的基础。那么,问题来了,javascript怎么面向对象编程呢?我想这很多老鸟也不定说的清楚吧,前端杂而活跃的各种社区,很多前端er一度依赖各种插件,根本不需要去了解这些深层的东西却照样能完成任务。但我想作为一个有追求的前端er还是很有必要去学习这些看似用不着的东西的,别的不说,就单单因为我们是前端er,只会用jquery一个稍微厉害的后端都能代替你。好吧,废话不多说,到底如何面向对象coding呢…..

很不幸,es5中并没有给出明确的定义‘类’的概念,所以传统的面向对象编程似乎是行不通的,那么又该肿么办呢?值得庆幸的是,前辈们通过不断探索总结,成功的用javascript模拟出了“类”。那么,javascript的类又该怎么定义呢?

在面向对象编程中,类(class)是对象(object)的模板,定义了同一组对象(又称”实例”)共有的属性和方法。

正文

说到javascript中的“类”就不得不说原型链和继承了,因为javascript是没有真正意义上的类的,所谓的类就是基于原型链和继承来实现的,即使ES6中加入了class,extends关键字实现类和继承,但实际上还是基于原型链和继承, ES6 类(class)是 JavaScript 现有的原型继承的语法糖。

1. 原型链和继承

在 javaScript 中,每个对象都有一个指向它的原型(prototype)对象的内部链接。这个原型对象又有自己的原型,直到某个对象的原型为 null 为止(也就是不再有原型指向),组成这条链的最后一环。这种一级一级的链结构就称为原型链(prototypechain)。
原型链

实际上在定义一个对象的时候原型链本身就已经生成了,javascript处处皆对象的思想放在这里理解起来就很容易了,看完后你会发现万物始于Object.prototype。那么我们都是如何定义一个对象的呢,博主总结的方法如下几个:

先初步来个demo具体解释下原型链是咋回事吧:

//someObject.[[Prototype]] 符号是用于指派 someObject 的原型。这个等同于 JavaScript 的 __proto__  属性(现已弃用)。。从 ES6 开始, [[Prototype]] 可以用Object.getPrototypeOf()和Object.setPrototypeOf()访问器来访问。这个方法可以放心使用博主亲测,主流浏览器已经支持了
// 假定有一个对象 o, 其自身的属性(own properties)有 a 和 b:
// {a: 1, b: 2}
// o 的原型 o.[[Prototype]]有属性 b 和 c:
// {b: 3, c: 4}
// 最后, o.[[Prototype]].[[Prototype]] 是 null.
// 这就是原型链的末尾,即 null,
// 根据定义,null 没有[[Prototype]].
// 综上,整个原型链如下:
// {a:1, b:2}---> {b:3, c:4} ---> null

来来来,撸袖子搞出个对象!!!(马上狗节,单身狗请自觉阅读完此篇博客)

  • 使用普通方法来创建一个对象

demo如下:

var o={
    a:0,
    b:function(){
        console.log(this.a)
    }
}
//创建一个对象
//原型链如下:
//o--->Object.prototype--->null
var a = ["yo", "whadup", "?"];
//创建一个数组对象
//(indexOf, forEach等方法都是从它继承而来).
//原型链如下:
//a--->Array.prototype ---> Object.prototype ---> null
function f(){
  return 1;
}
//创建一个函数对象
//(call, bind等方法都是从它继承而来):
//原型链如下:
//f ---> Function.prototype ---> Object.prototype ---> null
var date=new Date();
//创建一个日期对象
//原型链如下:
//date--->Date.prototype--->Object.pprototype--->null
  • 使用构造函数的方法

在 JavaScript 中,构造器其实就是一个普通的函数。当使用 new 操作符 来作用这个函数时,它就可以被称为构造方法(构造函数)。——MDN

demo如下:

function fun(){
    this.propA='prop';
    this.propB=3;
}
fun.prototype={
    methodA:function(){
        console.log(this.propA)
    }
}
var o=new fun();
o.methodA();//prop

// o是生成的对象,他的自身属性有'propA'和'propB'.
// 在o被实例化时,o.[[Prototype]]指向了fun.prototype.
  • 使用Object.create创建对象

ECMAScript 5 中引入了一个新方法:Object.create()。可以调用这个方法来创建一个新对象。新对象的原型就是调用 create 方法时传入的第一个参数:

var a = {a: 1};
// a ---> Object.prototype ---> null

var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (继承而来)继承概念下面会讲

var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null

var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); // undefined, 因为d没有继承Object.prototype
  • 使用 class 关键字

ECMAScript6 引入了一套新的关键字用来实现 class。使用基于类语言的开发人员会对这些结构感到熟悉,但它们是不一样的。 JavaScript 仍然是基于原型的。这些新的关键字包括 class, constructor, static, extends, 和 super.

"use strict";

class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}
//定义一个类Polygon
class Square extends Polygon {
  constructor(sideLength) {
    super(sideLength, sideLength);
  }//使用super引用父类
  get area() {
    return this.height * this.width;
  }
  set sideLength(newLength) {
    this.height = newLength;
    this.width = newLength;
  }
}
//使用extends定义Squeare继承父类Polygon
var square = new Square(2);//实例对象

//此时的原型链为:
//square--->Square.prototype--->Polygon.prototype--->Object.prototype--->null
//如果不理解为什么是这样,不要紧接着往下看类的说明
继承

其实在上面讲原型链的时候难以避免的也提到了继承,比如来自MDN的这个实例:

// 假定有一个对象 o, 其自身的属性(own properties)有 a 和 b:
// {a: 1, b: 2}
// o 的原型 o.[[Prototype]]有属性 b 和 c:(someObject.[[Prototype]] 符号是用于指派 someObject 的原型。这个等同于 JavaScript 的 __proto__  属性(现已弃用)。。从 ES6 开始, [[Prototype]] 可以用Object.getPrototypeOf()和Object.setPrototypeOf()访问器来访问。)
// {b: 3, c: 4}
// 最后, o.[[Prototype]].[[Prototype]] 是 null.
// 这就是原型链的末尾,即 null,
// 根据定义,null 没有[[Prototype]].
// 综上,整个原型链如下:
// {a:1, b:2} ---> {b:3, c:4} ---> null
console.log(o.a); // 1
// a是o的自身属性吗?是的,该属性的值为1

console.log(o.b); // 2
// b是o的自身属性吗?是的,该属性的值为2
// o.[[Prototype]]上还有一个'b'属性,但是它不会被访问到.这种情况称为"属性遮蔽 (property shadowing)".

console.log(o.c); // 4
// c是o的自身属性吗?不是,那看看o.[[Prototype]]上有没有.
// c是o.[[Prototype]]的自身属性吗?是的,该属性的值为4

console.log(o.d); // undefined
// d是o的自身属性吗?不是,那看看o.[[Prototype]]上有没有.
// d是o.[[Prototype]]的自身属性吗?不是,那看看o.[[Prototype]].[[Prototype]]上有没有.
// o.[[Prototype]].[[Prototype]]为null,停止搜索,
// 没有d属性,返回undefined

—-以上内容来自MDN继承与原型链

我想看到这里还是有些似懂非懂吧,那么来个例子吧:

var object1={
    a:1,
    b:function(){
        console.log(this.a+1);
    }
}
//定义一个对象
var object2=Object.create(object1);
//调用Object.create来创建一个新对象,新对象的原型就是调用 create 方法时传入的第一个参数,现在的原型链是:
//object2--->object1--->object1.prototype--->null
object2.d=4;
object2.a;
//1
//继承了object1的属性a
object2.b();
//2
//继承了object1的方法b
Object.getPrototypeOf(object2);
//object1得到object2的原型object1

我想现在应该是明白了吧,再不明白博主也是无能为力了,表达能力实在有限。

2. 类

总算说到类了,由于javascript的类基于原型链和继承,因此在上面的内容中就已经定义了很多的类。咱们javascript的类同样能实现传统类的多态,封装,继承等特性,这里主要讲解了继承这个概念,但实际上很多时候不经意可能就用了这三个特性。很好玩不是么

首先,我们先看下在ES5中定义一个类的形式:

function Animal(name) {
    this.name = name;
    this.sleep = function() {
        console.log(this.name+'正在睡觉');
    }
}//定义Animal类
Animal.prototype = {
    eat: function(food) {
        console.log(this.name+"正在吃"+food);
    }
}
function Cat() {

}
Cat.prototype = new Animal('Tom');
var Tom = new Cat('Tom');/Cat实例对象
Tom.eat('猫粮');
//Tom正在吃猫粮
//继承Animal方法
Tom.sleep();
//Tom正在睡觉
//继承Animal方法
//现在的原型链:
//Tom(Cat实例对象)--->Cat.prototype(Animal实例对象)--->Animal.prototype--->Object.prototype--->null

好的,然后我们看下在ES6中改写上面的类:

class Animal {
  constructor(name) {
    this.name = name;
  }

  sleep() {
    console.log(this.name + ' 正在睡觉');
  }
  eat(food){
    console.log(this.name+'正在吃'+food)  
  }
}

class Cat extends Animal {

}

const Tom = new Cat('Tom');
Tom.eat('猫粮');
//Tom正在吃猫粮
//继承Animal方法
Tom.sleep();
//Tom正在睡觉
//继承Animal方法
//现在的原型链:
//Tom(Cat实例对象)--->Cat.prototype(Animal实例对象)--->Animal.prototype--->Object.prototype--->null

定义一个类的方法实际上也是上面所说的定义一个对象的方法,类本身就是一个对象,只不过这个对象里面的方法和属性可以供许多实例对象调用而已。

后记

总的来说对于类的理解还是需要不断探索的,路漫漫其修远兮,吾将上下而求索。


关注我

我的微信公众号:前端开发博客,在后台回复以下关键字可以获取资源。

  • 回复「小抄」,领取Vue、JavaScript 和 WebComponent 小抄 PDF
  • 回复「Vue脑图」获取 Vue 相关脑图
  • 回复「思维图」获取 JavaScript 相关思维图
  • 回复「简历」获取简历制作建议
  • 回复「简历模板」获取精选的简历模板
  • 回复「加群」进入500人前端精英群
  • 回复「电子书」下载我整理的大量前端资源,含面试、Vue实战项目、CSS和JavaScript电子书等。
  • 回复「知识点」下载高清JavaScript知识点图谱

每日分享有用的前端开发知识,加我微信:caibaojian89 交流