JS面向对象和原型


本文总结于: 侯策《前端开发核心知识进阶》

new关键字做了什么

  • step1:首先创建一个空对象,这个对象将会作为执行 new 构造函数() 之后,返回的对象实例
  • step2:将上面创建的空对象的原型(proto),指向构造函数的 prototype 属性
  • step3:将这个空对象赋值给构造函数内部的 this,并执行构造函数逻辑
  • step4:根据构造函数执行逻辑,返回第一步创建的对象或者构造函数的显式返回值

newFunc的模拟训练

function Person(name) {
  this.name = name
}

const person = new newFunc(Person, 'lucas')

console.log(person)

// {name: "lucas"}

对newFunc的实现:

function newFunc(...args) {
  // 取出 args 数组第一个参数,即目标构造函数
  const constructor = args.shift()

  // 创建一个空对象,且这个空对象继承构造函数的 prototype 属性
  // 即实现 obj.__proto__ === constructor.prototype
  const obj = Object.create(constructor.prototype)

  // 执行构造函数,得到构造函数返回结果
  // 注意这里我们使用 apply,将构造函数内的 this 指向为 obj,注意:如果构造函数有返回值,则result为返回值,否则为undefined
  const result = constructor.apply(obj, args)

  // 如果造函数执行后,返回结果是对象类型,就直接返回,否则返回 obj 对象
  return (typeof result === 'object' && result != null) ? result : obj
}

上述三目运算的处理,其实和构造函数的显式返回有关:

function Person(name) {
  this.name = name
  return {1: 1}
}

const person = new Person(Person, 'lucas')

console.log(person)

// {1: 1}

如何优雅地实现继承

JS原型链

prototype

用构造函数生成实例对象,有一个缺点,那就是无法共享属性和方法。比如,在DOG对象的构造函数中,设置一个实例对象的共有属性species。

function DOG(name){

  this.name = name;

  this.species = '犬科';

}

然后,生成两个实例对象:

var dogA = new DOG('大毛');

var dogB = new DOG('二毛');

这两个对象的species属性是独立的,修改其中一个,不会影响到另一个。

dogA.species = '猫科';

alert(dogB.species); // 显示"犬科",不受dogA的影响

每一个实例对象,都有自己的属性和方法的副本。这不仅无法做到数据共享,也是极大的资源浪费。考虑到这一点,Brendan Eich决定为构造函数设置一个prototype属性。

这个属性包含一个对象(以下简称"prototype对象"),所有实例对象需要共享的属性和方法,都放在这个对象里面;那些不需要共享的属性和方法,就放在构造函数里面。实例对象一旦创建,将自动引用prototype对象的属性和方法。也就是说,实例对象的属性和方法,分成两种,一种是本地的,另一种是引用的。

function DOG(name){

  this.name = name;

}

DOG.prototype = { species : '犬科' };

var dogA = new DOG('大毛');

var dogB = new DOG('二毛');

alert(dogA.species); // 犬科

alert(dogB.species); // 犬科

现在,species属性放在prototype对象里,是两个实例对象共享的。只要修改了prototype对象,就会同时影响到两个实例对象。

__proto__

我们创建的每一个函数都有一个 prototype 属性,这个属性是一个指针,指向一个对象。这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法,简单来说,该函数实例化的所有对象的__proto__的属性指向这个对象,它是该函数所有实例化对象的原型。

通用规则:

  • 对象有__proto__属性,函数有prototype属性;
  • 对象由函数生成;
  • 生成对象时,对象的__proto__属性指向函数的prototype属性。
//创建空对象时,实际上我们是用Object函数来生成对象的:
var o = {}
o.__proto__ === Object.prototype
//true

//我们也可以显式的使用Object函数来创建对象:
var o = Object()
o.__proto__ === Object.prototype
//true

//当我们使用函数来创建自定义的对象时,上面的规则同样适用:
function MyObj(){}
typeof MyObj
//"function"
var mo = new MyObj()
mo.__proto__ === MyObj.prototype
//true

既然JavaScript里“一切皆对象”,那函数自然也是对象的一种。对于函数作为对象来说,上面的规则同样适用:

//函数对象都是由Function函数生成的:
function fn(){}
fn.__proto__ === Function.prototype
//true

//Function函数本身作为对象时,生成它的函数是他自身!
Function.__proto__ === Function.prototype
//true

//Object函数既然是函数,那生成它的函数自然是Function函数咯:
Object.__proto__ === Function.prototype
//true

prototype与__proto__

对象的__proto__属性是从生成它的函数的prototype那里得来的
一般函数默认的prototype是一个类型为"object"的对象,它有两个属性:constructor__proto__。其中constructor属性指向这个函数自身,__proto__属性指向Object.prototype,这说明一般函数的prototype属性是由Object函数生成的。
大多数情况下,__proto__可以理解为“构造器的原型”,即__proto__===constructor.protype
特殊情况:
Object函数特殊情况:Object.prototype.__proto__=== null,我们知道,这就是JavaScript原型链的终点了。
Fuction函数特殊情况:Function.prototype.__proto__ === Object.prototype Function函数的prototype属性是一个"function"类型的对象,而不像其他函数是类型为"object"的对象。
为什么要这样设定呢?
typeof Object.prototype === "object",说明它是一个Object对象,如果它由Object函数生成,于是按照我们上面的通用规则,就该是Object.prototype.__proto__ === Object.prototype
啊哈,问题出现了,Object.prototype.__proto__属性指向了它自身,这样以__proto__属性构成的原型链就再也没有终点了!所以为了让原型链有终点,在原型链的最顶端,JavaScript规定了Object.prototype.__proto__=== null
按照我们最开始提出的通用规则,一个"function"类型的对象,应该是由Function函数生成的,那它的prototype属性应该指向Function.prototype,也就是Function.prototype.__proto__=== Function.prototype。和Object函数同样的问题出现了:循环引用。所以JavaScript规定Function.prototype.__proto__=== Object.prototype,这样既避免了出现循环引用,又让__proto__构成的原型链指向了唯一的终点:Object.prototype.__proto__ === null

原型链

在这里插入图片描述

JS原型继承

构造函数的继承

  function Animal(){

    this.species = "动物";

  }
  function Cat(name,color){

    this.name = name;

    this.color = color;

  }
构造函数绑定

第一种方法也是最简单的方法,使用call或apply方法,将父对象的构造函数绑定在子对象上,即在子对象构造函数中加一行:

function Cat(name,color){

    Animal.apply(this, arguments);

    this.name = name;

    this.color = color;

  }

  var cat1 = new Cat("大毛","黄色");

  alert(cat1.species); // 动物
prototype模式、

第二种方法更常见,使用prototype属性。
如果"猫"的prototype对象,指向一个Animal的实例,那么所有"猫"的实例,就能继承Animal了。

Cat.prototype = new Animal();

   //避免原型链紊乱
  Cat.prototype.constructor = Cat;

  var cat1 = new Cat("大毛","黄色");

  alert(cat1.species); // 动物
直接继承prototype

第三种方法是对第二种方法的改进。由于Animal对象中,不变的属性都可以直接写入Animal.prototype。所以,我们也可以让Cat()跳过 Animal(),直接继承Animal.prototype。

现在,我们先将Animal对象改写:

function Animal(){ }

  Animal.prototype.species = "动物";

然后,将Cat的prototype对象,然后指向Animal的prototype对象,这样就完成了继承。

Cat.prototype = Animal.prototype;

Cat.prototype.constructor = Cat;

var cat1 = new Cat("大毛","黄色");

alert(cat1.species); // 动物

与前一种方法相比,这样做的优点是效率比较高(不用执行和建立Animal的实例了),比较省内存。缺点是 Cat.prototype和Animal.prototype现在指向了同一个对象,那么任何对Cat.prototype的修改,都会反映到Animal.prototype。

所以,上面这一段代码其实是有问题的。请看第二行

Cat.prototype.constructor = Cat;

这一句实际上把Animal.prototype对象的constructor属性也改掉了!

利用空对象作为中介

由于"直接继承prototype"存在上述的缺点,所以就有第四种方法,利用一个空对象作为中介。其实有点相当于第二种方法。

var F = function(){};

  F.prototype = Animal.prototype;

  Cat.prototype = new F();

  Cat.prototype.constructor = Cat;

F是空对象,所以几乎不占内存。这时,修改Cat的prototype对象,就不会影响到Animal的prototype对象。

function extend(Child, Parent) {

    var F = function(){};

    F.prototype = Parent.prototype;

    Child.prototype = new F();

    Child.prototype.constructor = Child;

    Child.uber = Parent.prototype;

  }

   extend(Cat,Animal);

  var cat1 = new Cat("大毛","黄色");

  alert(cat1.species); // 动物

这个extend函数,就是YUI库如何实现继承的方法。

Child.uber = Parent.prototype; 意思是为子对象设一个uber属性,这个属性直接指向父对象的prototype属性。(uber是一个德语词,意思是"向上"、“上一层”。)这等于在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。

拷贝继承

上面是采用prototype对象,实现继承。我们也可以换一种思路,纯粹采用"拷贝"方法实现继承。简单说,如果把父对象的所有属性和方法,拷贝进子对象,不也能够实现继承吗?这样我们就有了第五种方法。

首先,还是把Animal的所有不变属性,都放到它的prototype对象上。

function Animal(){}

  Animal.prototype.species = "动物";

然后,再写一个函数,实现属性拷贝的目的。

function extend2(Child, Parent) {

    var p = Parent.prototype;

    var c = Child.prototype;

    for (var i in p) {

      c[i] = p[i];

      }

    c.uber = p;

  }

使用的时候这么写:

extend2(Cat, Animal);

  var cat1 = new Cat("大毛","黄色");

  alert(cat1.species); // 动物

但是请注意,上述的属性拷贝为浅拷贝,会存在问题!

非构造函数的继承

 var Chinese = {
    nation:'中国'
  };
  
  var Doctor ={
    career:'医生'
  }
object()方法
function object(o) {

    function F() {}

    F.prototype = o;

    return new F();

  }

    //使用时第一步先在父对象的基础上,生成子对象
    var Doctor = object(Chinese);
    //再加上子对象本身的属性
    Doctor.career = '医生';
浅拷贝
function extendCopy(p) {

    var c = {};

    for (var i in p) {
      c[i] = p[i];
    }

    c.uber = p;

    return c;
  }

     //使用时
      var Doctor = extendCopy(Chinese);

  Doctor.career = '医生';

  alert(Doctor.nation); // 中国

但是,这样的拷贝有一个问题。那就是,如果父对象的属性等于数组或另一个对象,那么实际上,子对象获得的只是一个内存地址,而不是真正拷贝,因此存在父对象被篡改的可能。

深拷贝
function deepCopy(p, c) {

    var c = c || {};

    for (var i in p) {

      if (typeof p[i] === 'object') {

        c[i] = (p[i].constructor === Array) ? [] : {};

        deepCopy(p[i], c[i]);

      } else {

         c[i] = p[i];

      }
    }

    return c;
  }

它的实现并不难,只要递归调用"浅拷贝"就行了。目前,jQuery库使用的就是这种继承方法。

JS创建对象的几种方式

工厂模式

工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽离了创建对象的具体过程。考虑到在 ECMAScript 中无法创建类,开发人员发明以一种函数,用函数来封装以特定接口创建对象的细节。如下面的例子所示

function createPerson(name, age, job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        alert(this.name);
    };
    return o;
}

var person1 = createPerson("james"9"student");

var person2 = createPerson("kobe"9"student");

优点:解决了创建多个相似对象时,代码的复用问题

缺点:使用工厂模式创建的对象,没有解决对象识别的问题(就是怎样知道一个对象的类型是什么)

构造函数模式

构造函数模式在前面的原型继承中都有涉及,这里就不多做展开了。

function createPerson(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){
        alert(this.name);
    };
    return o;
}

var person1 = new createPerson("james"9"student");

var person2 = new createPerson("kobe"9"student");

优点:解决了工厂模式中对象类型无法识别的问题,并且创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型。
缺点:我们知道 ECMAScript 中的函数是对象,在使用构造函数创建对象时,每个方法都会在实例对象中重新创建一遍。拿上面的例子举例,这意味着每创建一个对象,我们就会创建一个 sayName 函数的实例,但它们其实做的都是同样的工作,因此这样便会造成内存的浪费。

原型模式

我们知道,我们创建的每一个函数都有一个 prototype 属性,这个属性指向函数的原型对象,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。我们通过使用原型对象可以让所有的对象实例共享它所包含的属性和方法,因此这样也解决了代码的复用问题。

function Person(){

}

Person.prototype.name = "james";
Person.prototype.age = 9;
Person.prototype.job = "student";
Person.prototype.sayName = function(){
    alert(this.name);
}

var person1 = new Person();
person1.sayName(); // "james"

var person2 = new Person();
person2.sayName(); // "james"


console.log(person1.sayName === person2.sayName)

优点:解决了构造函数模式中多次创建相同函数对象的问题,所有的实例可以共享同一组属性和函数。

缺点:

  • 首先第一个问题是原型模式省略了构造函数模式传递初始化参数的过程,所有的实例在默认情况下都会取得默认的属性值,会在一定程度上造成不方便。
  • 因为所有的实例都是共享一组属性,对于包含基本值的属性来说没有问题,但是对于包含引用类型的值来说(例如数组对象),所有的实例都是对同一个引用类型进行操作,那么属性的操作就不是独立的,最后导致读写的混乱。我们创建的实例一般都是要有属于自己的全部属性的,因此单独使用原型模式的情况是很少存在的。
组合使用构造函数模式和原型模式

创建自定义类型的最常见方式,就是组合使用构造函数模式和原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。使用这种模式的好处就是,每个实例都会拥有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。而且这中混成模式还支持向构造函数传递参数,可以说是及两种模式之长。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
}

Person.prototype = {
    constructor: Person,
    sayName: function(){
        alert(this.name);
    }
}

var person1 = new createPerson("james"9"student");

var person2 = new createPerson("kobe"9"student");

console.log(person1.name); // "james"
console.log(person2.name); // "kobe"
console.log(person1.sayName === person2.sayName); // true

优点:采用了构造函数模式和原型模式的优点,这种混成模式是目前使用最广泛,认同度最高的一种创建自定类型的方法。

缺点:由于使用了两种模式,因此对于代码的封装性来说不是很好。

动态原型模式

由于上面混成模式存在封装性的问题,动态原型模式是解决这个问题的一个方案。这个方法把所有信息都封装到了构造函数中,而在构造函数中通过判断只初始化一次原型。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;

    if(typeof this.sayName !== "function" ){

        Person.prototype.sayName: function(){
            alert(this.name);
        } 
    } 
}

var person1 = new createPerson("james"9"student");

person1.sayName(); // "james"

注意在 if 语句中检查的可以是初始化后应该存在的任何属性或方法,不必要检查每一个方法和属性,只需要检查一个就行。

优点:解决了混成模式中封装性的问题

寄生构造函数模式

如果在前面几种模式不适用的情况下,可以使用寄生构造函数模式。这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后返回新创建的对象。

function Person(name, age, job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        alert(this.name);
    };
    return o;
}

var person1 = new Person("james"9"student");

那么两者有什么功能上的差别呢?事实上,两者本质上的差别仅在于new操作符(因为函数取什么名字无关紧要),工厂模式创建对象时将createPerson看作是普通的函数,而寄生构造函数模式创建对象时将Person看作是构造函数,不过这对于创建出的对象来说,没有任何差别。
注意在构造函数不返回值的情况下,默认会返回新创建的对象,而通过在构造函数的末尾添加一个 return 语句,可以重写调用构造函数时返回的值。

构造函数和普通函数的区别在于:当使用new+构造函数创建对象时,如果构造函数内部没有return语句,那么默认情况下构造函数将返回一个该类型的实例(如果以上面的例子为参考,person1和person2为Person类型的对象实例,可以使用person1 instanceof Person检验),但如果构造函数内部通过return语句返回了一个其它类型的对象实例,那么这种默认的设置将被打破,构造函数最终返回的实例类型将以return语句中对象实例的类型为准。

基于这个规则,在Person()构造函数中,由于最后通过return语句返回了一个Object类型的对象实例,所以通过该构造函数创建的对象实际上是Object类型而不是Person类型;这样一来就与createPerson()函数返回的对象类型相同,因此可以说工厂模式和寄生构造函数模式在功能上是等价的。

如果非要说两者的不同,并且要从其中选择一个作为创建对象的方法的话,我个人更偏向于寄生构造函数模式一些。这是因为new Person()(寄生构造函数模式)更能让我感觉到自己正在创建一个对象,而不是在调用一个函数(工厂模式)。
优点:我对这个模式的理解是,它主要是基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函数,也达到了扩展对象的目的。

缺点:和工厂模式一样的问题,不能依赖 instanceof 操作符来确定对象的类型。

稳妥构造函数模式

Douglas Crockford 发明了 JavaScript 中的稳妥对象这个概念。所谓稳妥对象,指的就是,没有公共属性,而且其方法也不使用 this 的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用 this 和 new),或者在防止数据被其他应用程序改动时使用。

稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建的对象的实例方法不引用 this ;二是不使用 new 操作符调用构造函数。因此我们可以将前面的例子改写如下:

function Person(para_name, para_age, para_job) {
    //创建要返回的对象
    var o = {};

    //在这里定义私有属性和方法
    var name = para_name;
    var age = para_age;
    var job = para_job;

    var sayAge = function() {
        alert(age);
    };

    //在这里定义公共方法
    o.sayName = function() {
        alert(name);
    };

    //返回对象
    return o;
}

var person1 = Person("Nicholas", 29, "Software Engineer");    //创建对象实例
person1.sayName();    //Nicholas
person1.name;         //undefined
person1.sayAge();     // 报错

稳妥构造函数模式与前面介绍的两种设计模式具有相似的地方,都是在函数内部定义好对象之后返回该对象来创建实例。然而稳妥构造函数模式的独特之处在于具有以下特点:

  • 没有通过对象定义公共属性
  • 在公共方法中不使用this引用对象自身
  • 不使用new操作符调用构造函数
  • 这种设计模式最适合在一些安全的环境中使用(这些环境中会禁止使用this和new);为了较好地理解这种设计模式,我们可以采取类比的方法——这种构造对象的方式就如同C++/Java语言中通过访问控制符private定义出包含私有成员的类的方式一样(将上例按C++中类的方式来定义):
class Person {
//定义私有成员变量和函数
private: 
    string name;
    int age;
    string job;
    int sayAge() {return age;}
//定义构造函数和公共方法(函数)
public:
    string sayName() {return name;}    //公共方法
    Person(string p_name, int p_age, string p_job):name(p_name),age(p_age),job(p_job) {}  //构造函数
}

//创建对象实例
Person person1("Nicholas", 29, "Software Engineer");
person1.sayName();    //Nicholas
person1.name;         //报错(无法访问)
person1.sayAge();     //报错(无法访问)

可见,利用C++定义出了一个Person类,其中的name、age、job以及sayAge()是私有成员,无法通过类似person1.name的方式直接访问,这是一种类的保护机制;而定义为public的sayName()函数则可以直接访问。

JS中的稳妥构造函数模式正是为了实现这样的数据保护机制。它巧妙地利用了函数的作用域实现了对象属性的私有化:在函数中定义的变量是局部变量,按道理本应该在函数执行完毕退出后进行销毁或清理,但由于通过对象的公共方法对该局部变量保持着引用,所以该变量即便是在构造函数退出之后也依然保持有效(闭包)。

这样一来,创建出的对象既能通过公共方法提供的访问接口对私有属性进行访问(引用的是构造函数的局部变量),也能保证无法通过对象自身对其直接访问(person1.name无法访问到对应数据,因为name是构造函数的局部变量而不是对象的属性),从而保证了对象属性的访问安全。

总结

下面就是对知识的进一步梳理总结:

其中原型链实现继承最关键的要点是:

Child.prototype = new Parent()

构造函数实现继承的要点是:

function Child (args) {
    // ...
    Parent.call(this, args)
}

这样的实现其实只是实现了实例属性继承,Parent 原型的方法在 Child 实例中并不可用。

组合继承的实现要点是:

function Child (args1, args2) {
    // ...
    this.args2 = args2
    Parent.call(this, args1)
}
Child.prototype = new Parent()
Child.prototype.constrcutor = Child

Child 实例会存在 Parent 的实例属性。因为我们在 Child 构造函数中执行了 Parent 构造函数。同时,Child.proto 也会存在同样的 Parent 的实例属性,且所有 Child 实例的 proto 指向同一内存地址。同时上述实现也都没有对静态属性的继承。

一个比较完整的实现:

function inherit(Child, Parent) {
     // 继承原型上的属性 
    Child.prototype = Object.create(Parent.prototype)

     // 修复 constructor
    Child.prototype.constructor = Child

    // 存储超类
    Child.super = Parent

    // 静态属性继承
    if (Object.setPrototypeOf) {
        // setPrototypeOf es6
        Object.setPrototypeOf(Child, Parent)
    } else if (Child.__proto__) {
        // __proto__ es6 引入,但是部分浏览器早已支持
        Child.__proto__ = Parent
    } else {
        // 兼容 IE10 等陈旧浏览器
        // 将 Parent 上的静态属性和方法拷贝一份到 Child 上,不会覆盖 Child 上的方法
        for (var k in Parent) {
            if (Parent.hasOwnProperty(k) && !(k in Child)) {
                Child[k] = Parent[k]
            }
        }
    }

}

静态属性继承存在一个问题:在陈旧浏览器中,属性和方法的继承我们是静态拷贝的,继承完后续父类的改动不会自动同步到子类。这是不同于正常面向对象思想的。但是这种组合式继承,已经相对完美、优雅。

小细节:这种继承方式无法实现对 Date 对象的继承,因为: JavaScript 的日期对象只能通过 JavaScript Date 作为构造函数来实例化得到。

如何实现对 Date 的继承呢?

function DateConstructor() {
    var dateObj = new(Function.prototype.bind.apply(Date, [Date].concat(Array.prototype.slice.call(arguments))))()
  //实现DateConstructor.prototype.__proto__ === Date.prototype
    Object.setPrototypeOf(dateObj, DateConstructor.prototype)

    dateObj.foo = 'bar'

    return dateObj
}

Object.setPrototypeOf(DateConstructor.prototype, Date.prototype)

DateConstructor.prototype.getMyTime = function getTime() {
    return this.getTime()
}

let date = new DateConstructor()

console.log(date.getMyTime())

ES6实现对Date的继承:

class DateConstructor extends Date {
    constructor() {
        super()
        this.foo ='bar'
    }
    getMyTime() {
        return this.getTime()
    }
}

let date = new DateConstructor()

date.getMyTime() // 1558921640586

缺点:Babel 并没有对继承 Date 进行特殊处理,无法做到兼容。

Babel 编译结果研究

class Person {
    constructor(){
        this.type = 'person'
    }
}

class Student extends Person {
    constructor(){
        super()
    }
}

var student1 = new Student() student1.type // "person"

student1 instanceof Student // true student1 instanceof Person // true student1.hasOwnProperty('type') // true

Babel 编译后的代码

var Person = function Person() {
    _classCallCheck(this, Person);
    this.type = 'person';
};

// 实现定义 Student 构造函数,它是一个自执行函数,接受父类构造函数为参数
var Student = (function(_Person) {
    // 实现对父类原型链属性的继承
    _inherits(Student, _Person);

    // 将会返回这个函数作为完整的 Student 构造函数
    function Student() {
        // 使用检测
        _classCallCheck(this, Student);  
        // _get 的返回值可以先理解为父类构造函数       
        _get(Object.getPrototypeOf(Student.prototype), 'constructor', this).call(this);
    }

    return Student;
})(Person);

// _x为Student.prototype.__proto__
// _x2为'constructor'
// _x3为this
var _get = function get(_x, _x2, _x3) {
    var _again = true;
    _function: while (_again) {
        var object = _x,
            property = _x2,
            receiver = _x3;
        _again = false;
        // Student.prototype.__proto__为null的处理
        if (object === null) object = Function.prototype;
        // 以下是为了完整复制父类原型链上的属性,包括属性特性的描述符
        var desc = Object.getOwnPropertyDescriptor(object, property);
        if (desc === undefined) {
            var parent = Object.getPrototypeOf(object);
            if (parent === null) {
                return undefined;
            } else {
                _x = parent;
                _x2 = property;
                _x3 = receiver;
                _again = true;
                desc = parent = undefined;
                continue _function;
            }
        } else if ('value' in desc) {
            return desc.value;
        } else {
            var getter = desc.get;
            if (getter === undefined) {
                return undefined;
            }
            return getter.call(receiver);
        }
    }
};

//让 Student 子类继承 Person 父类原型链上的方法
function _inherits(subClass, superClass) {
    // superClass 需要为函数类型,否则会报错
    if (typeof superClass !== 'function' && superClass !== null) {
        throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass);
    }
    // Object.create 第二个参数是为了修复子类的 constructor
    subClass.prototype = Object.create(superClass && superClass.prototype, {
        constructor: {
            value: subClass,
            enumerable: false,
            writable: true,
            configurable: true
        }
    });
    // Object.setPrototypeOf 是否存在做了一个判断,否则使用 __proto__
    if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}

jQuery 中的对象思想

const pNodes = $('p')
// 我们得到一个数组
const divNodes= $('div')
// 我们得到一个数组
const pNodes = $('p')
pNodes.addClass('className')
//数组上可是没有 addClass 方法
$('p')
//$是一个函数
$.ajax()
//$是一个对象

查看$的源码:

var jQuery = (function(){
    var $

    // ...

    $ = function(selector, context) {
        return function (selector, context) {
            var dom = []
            dom.__proto__ = $.fn

            // ...

            return dom
        }
    }

    $.fn = {
        addClass: function() {
            // ...
        },
        // ...
    }

    $.ajax = function() {
        // ...
    }

    return $
})()

window.jQuery = jQuery
window.$ === undefined && (window.$ = jQuery)

当调用 $(‘p’) 时,最终返回的是 dom,而 dom.proto 指向了 . f n , .fn, .fn.fn 是包含了多种方法的对象集合。因此返回的结果(dom)可以在其原型链上找到 addClass 这样的方法。

同时 ajax 方法直接挂载在构造函数 $ 上,它是一个静态属性方法。

这个很微妙地表现出来了jQuery面向对象的精妙设计。

类继承和原型继承的区别

传统的面向对象语言的类继承,引发的一些问题:

紧耦合问题
脆弱基类问题
层级僵化问题
必然重复性问题
大猩猩—香蕉问题
对于类继承和原型继承的区别,我们可以参看这篇文章:
类继承和原型继承的区别

参考资料:

http://www.ruanyifeng.com/blog/2011/06/designing_ideas_of_inheritance_mechanism_in_javascript.html
http://cavszhouyou.top/JavaScript%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E4%B9%8B%E5%8E%9F%E5%9E%8B%E4%B8%8E%E5%8E%9F%E5%9E%8B%E9%93%BE.html
https://www.jianshu.com/p/686b61c4a43d
https://www.jianshu.com/p/08c07a953fa0
http://www.ruanyifeng.com/blog/2010/05/object-oriented_javascript_inheritance_continued.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值