原型、构造函数与面向对象编程

1. 复杂的原型链

原型是 JavaScript 向面向对象编程语言进化的产物。

为什么要面向对象编程?为了代码复用,为了抽象出类似的概念,为了让代码更有逻辑,更符合我们本身的世界观。

在我们的意识中,其实世界的事物都是一个一个对象。例如,男人和女人,都属于人的一个子分支,他们两者都具有人的一些共同点特点,或者说,男人和女人这两个对象都“继承”了“人”这个对象的一些特点,而又有各自的不一样属性。或者说:人,就是男人和女人的原型。

在 JavaScript 中,Function ,Object 分别是函数,对象的原型,而其实函数也是一个特殊的对象,因此函数原型创作的函数也是特殊的对象。

var fun =function() {
  console.log('this is a function');
};

var obj = {
  string: 'obj',
};

console.log(Function.prototype.isPrototypeOf(fun));
console.log(Object.prototype.isPrototypeOf(fun));

console.log(Function.prototype.isPrototypeOf(obj));
console.log(Object.prototype.isPrototypeOf(obj));

console.log(Object.prototype.isPrototypeOf(Function));

// true, true, true, false, true

利用Object.prototype.isPrototypeOf(targetObj)方法,可以检测Object测试对象是否在检测目标的原型链上,根据输出可以看到,Function 对象和 Object 对象在所有函数的原型链上,其中ObjectFunction的原型,而Object是所有对象的原型,包括 Function对象。

我们可以使用一个 ES5 的方法来获取一个对象的原型:
Object.getPrototypeOf(obj)

var Person = function (name) {
  this.name = name;
};
var alice = new Person('alice');

console.log(Object.getPrototypeOf(alice));
console.log(Object.getPrototypeOf(alice) === Person.prototype);
// Person.prototype, true

可以看到,Person 原型,生成了一个实例 alice,因此Person对象 是alice的原型。

类似的,Person也应该有原型,所谓原型,就是生成它的那个角色。上面实例中,Person 是一个函数,那它的原型是什么呢?我们用代码探究:

console.log(Object.getPrototypeOf(Person));
console.log(Object.getPrototypeOf(Person) === Function.prototype);
// ƒ () { [native code] } , true

由于创造函数的函数(Function.prototype)是 JavaScript 语言的源代码,因此不存在它是什么,但是我们通过第二个判断得知(三个等号),生产函数的函数就是Function.prototype

Function.prototype本身是一个对象,因此它也是有原型的,而它的原型就是创造对象的对象,Object.prototype。

console.log(
  Object.getPrototypeOf(Function.prototype) === Object.prototype);
// true

Object.prototype 有没有原型呢?有的,为了避免原型链没有尽头,ES 规定了Object.prototype的原型就是 null

因此我们可以画出原型链:
alice -> Person -> Function -> Object -> null

prototype chain

原型链之所以是复杂的,就是因为在代码构建工程的过程中,可以会产生无数的原型和原型生成的实际对象,而原型可能又会有它的原型,等等,一层层的关系形成一个链条,这就是原型链。

但是原型链的灵活也在于此:一个实例可以调用它的原型的方法,以及原型的原型的方法。

也就是它在使用函数时,会在自身的属性/方法中查找,如果没有找到该方法(函数),它就会沿着原型链,查找它的原型中的方法,如果仍没有,继续沿原型链进行查找,知道原型链的尽头:null

可以看到,所有的对象都是FunctionObject原型的下游,因此,所有的函数生产对象都可以调用 Function.prototypeObject.prototype中的方法,call(this, arg1,arg2,...), apply(this,arg[]), bind(this), toString(), isPropertyOf(), hasOwnProperty()

2. 构造函数与 new 命令

其实上面我们已经涉及到了构造函数。构造函数的实现没有脱离原型链这一基础,只是在语法实现上来说,更靠近其他语言例如 Java 的写法。一个典型的构造函数:

function Person(name, email) {
  this.name = name;
  this.email = email;
  
  this.sayName =function() {
    console.log(this.name);
  };
  
  this.sayEmail = function() {
    console.log(this.email);
  };
}

一个典型的构造函数,有它的属性,name, email,有它的方法,sayName(), sayEmail(),他的实例都会具有它的这些方法 由它可以生出很多实例,但是需要配合 一个关键词使用:new,这也是借鉴 Java 语言中的写法。

// Java class
public class Dog {
  string name;
  int age;
  string color;

  public barking() {
    System.out.println("wo-wo-wo"); 
  }
}

Dog myDog = new Dog();
myDog.barking();

类似的,在 JavaScript 中,构造函数就相当于是 Java 中的类,它们都可以生成一个实例,并且实例可以调用构造函数中的方法。

var alice = new Person('alice', 'alice@google.com');

alice.sayName();
alice.sayEmail();
// "alice"  "alice@google.com"

这其中涉及到 new 运算符的实现,大致的实现步骤来说应该是这样:

  1. 生成一个新的空对象。
  2. 将1.中生成的空对象的原型指向构造函数的prototype属性。
  3. 将这个空对象赋值给函数内部中的this关键字。
  4. 开始执行构造函数内部的代码。
  5. 执行完毕之后,如果原构造函数没有返回其他对象,则返回this

此时的this值就指向当前的实例,本例中就是alice

面试有可能会面的:实现一个 new 命令。

根据上面的四步,实现一个 new 命令:

// constructor 是构造函数,params是传入构造函数的参数
function _new (constructor, params) {
  var args = [...arguments]; // 将传入参数变成数组
  var constructor  = args.shift(); // 取出构造函数
  var context = Object.create(contructor.ptototype); 
  /* 以构造函数的prototype为原型,创建一个prototpye对象,新建的对象具有所有构造函数的prototype的属性和方法。 */

  var result  = constructor.apply(context, args); //执行构造函数
  return (typeof result === 'object' && result!==null) ? result : context; 
// 如执行结果非空且类型为对象,则返回该结果,否则,返回新创建的对象。
}

3. ES6 的 class

ES6 引入的 class 类的定义方法,从语法上更靠近了基于类的编程语言,实际上它的运行机制仍是基于原型的。

class Person {
  constructor(name, email) {
    this.name = name;
    this.email = email;   
  }

  // Methods
  sayName () {
    console.log(this.name);
  };
  sayEmail() {
    console.log(this.email);
  };
}

class的语法中,基本的变量数据放到 constructor构造数据中,而其他的成员函数,可以直接写成函数的形式,不需要function关键词,调用的时候直接在前面加上this.

class 还提供常用的gettersetter可以使用。因此,settergetter配合methods可以令function+new的奇怪组合消失了,让上帝的归上帝,凯撒的归凯撒,function回归到它定义函数的本意,而不是定义一个'类'(虽然实际上仍是原型构造函数)。

  set setJob(offer) {
    this.offer = offer;
  }
  
  get job() {
    return this.upperName();
  }

  upperName() {
    return this.offer.toUpperCase();
  }  
  var alice = new Person('alice', 'alice@google.com');

  alice.setJob = 'Front-end';
  console.log(alice.job);  // "FRONT-END"

而且class的继承也更像继承了。一个子类可以继承父类,直接使用extends关键字,就像在别的基于类的函数(Java, C++)一样。

class Woman extends Person {
  constructor(name, mail) {
    super(name, mail);
    this.name = name;
    this.mail = mail;

    this.organ = 'vaginal';
  }
  
  specialOrgan () {
    console.log(this.name + ' has '+ this.organ);
  }
}

var alice = new Woman('alice', 'alice@google.com');
alice.setJob = 'Front-end';
console.log(alice.job);
alice.specialOrgan();
//  "FRONT-END"  "alice has vaginal"

在继承的子类Woman中,在调用父类中的成员数据前,需要调用super关键词包括所有需要调用的数据,否则会出现引用错误。上面代码可以看到,子类继承了父类的settergetter,而且子类有自己的方法specialOrgan(),并且在子类的方法中成功地调用了父类的name属性。

代码调试地址
代码调试地址

5. 参考阅读

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348

推荐阅读更多精彩内容