乐趣区

轻松理解JS中的面向对象顺便搞懂prototype和proto

这篇文章次要讲一下 JS 中面向对象以及 __proto__ptototypeconstructor,这几个概念都是相干的,所以一起讲了。

在讲这个之前咱们先来说说类,理解面向对象的敌人应该都晓得,如果我要定义一个通用的类型我能够应用类(class)。比方在 java 中咱们能够这样定义一个类:

public class Puppy{
    int puppyAge;

    public Puppy(age){puppyAge = age;}
  
    public void say() {System.out.println("汪汪汪"); 
    }
}

上述代码咱们定义了一个 Puppy 类,这个类有一个属性是 puppyAge,也就是小狗的年龄,而后有一个构造函数 Puppy(),这个构造函数接管一个参数,能够设置小狗的年龄,另外还有一个谈话的函数 say。这是一个通用的类,当咱们须要一个两岁的小狗实例是间接这样写,这个实例同时具备父类的办法:

Puppy myPuppy = new Puppy(2);
myPuppy.say();     // 汪汪汪

然而晚期的 JS 没有 class 关键字啊(以下说 JS 没有 class 关键字都是指 ES6 之前的 JS,次要帮忙大家了解概念),JS 为了反对面向对象,应用了一种比拟波折的形式,这也是导致大家蛊惑的中央,其实咱们将这种形式跟个别的面向对象类比起来就很清晰了。上面咱们来看看 JS 为了反对面向对象须要解决哪些问题,都用了什么波折的形式来解决。

没有 class,用函数代替

首先 JS 连 class 关键字都没有,怎么办呢?用函数代替,JS 中最不缺的就是函数,函数不仅可能执行一般性能,还能当 class 应用。比方咱们要用 JS 建一个小狗的类怎么写呢?间接写一个函数就行:

function Puppy() {}

这个函数能够间接用 new 关键字生成实例:

const myPuppy = new Puppy();

这样咱们也有了一个小狗实例,然而咱们没有构造函数,不能设置小狗年龄啊。

函数自身就是构造函数

当做类用的函数自身也是一个函数,而且他就是默认的构造函数。咱们想让 Puppy 函数可能设置实例的年龄,只有让他接管参数就行了。

function Puppy(age) {this.puppyAge = age;}

// 实例化时能够传年龄参数了
const myPuppy = new Puppy(2);

留神下面代码的 this,被作为类应用的函数外面 this 总是指向实例化对象,也就是 myPuppy。这么设计的目标就是让使用者能够通过构造函数给实例对象设置属性,这时候 console 进去看myPuppy.puppyAge 就是 2。

console.log(myPuppy.puppyAge);   // 输入是 2

实例办法用 prototype

下面咱们实现了类和构造函数,然而类办法呢?Java 版小狗还能够“汪汪汪”叫呢,JS 版怎么办呢?JS 给出的解决方案是给办法增加一个 prototype 属性,挂载在这下面的办法,在实例化的时候会给到实例对象。咱们想要 myPuppy 能谈话,就须要往 Puppy.prototype 增加谈话的办法。

Puppy.prototype.say = function() {console.log("汪汪汪");
}

应用 new 关键字产生的实例都有类的 prototype 上的属性和办法,咱们在 Puppy.prototype 上增加了 say 办法,myPuppy 就能够谈话了,我么来试一下:

myPuppy.say();    // 汪汪汪

实例办法查找用__proto__

那 myPuppy 怎么就可能调用 say 办法了呢,咱们把他打印进去看下,这个对象上并没有 say 啊,这是从哪里来的呢?

这就该 __proto__ 上场了,当你拜访一个对象上没有的属性时,比方 myPuppy.say,对象会去__proto__ 查找。__proto__的值就等于父类的 prototype, myPuppy.__proto__指向了Puppy.prototype

如果你拜访的属性在 Puppy.prototype 也不存在,那又会持续往 Puppy.prototype.__proto__ 上找,这时候其实就找到了 Object.prototype 了,Object.prototype再往上找就没有了,也就是 null,这其实就是原型链

constructor

咱们说的 constructor 个别指类的 prototype.constructorprototype.constructor 是 prototype 上的一个保留属性,这个属性就指向类函数自身,用于批示以后类的构造函数。

既然 prototype.constructor 是指向构造函数的一个指针,那咱们是不是能够通过它来批改构造函数呢?咱们来试试就晓得了。咱们先批改下这个函数,而后新建一个实例看看成果:

function Puppy(age) {this.puppyAge = age;}

Puppy.prototype.constructor = function myConstructor(age) {this.puppyAge = age + 1;}

const myPuppy2 = new Puppy(2);
console.log(myPuppy2.puppyAge);    // 输入是 2 

上例阐明,咱们批改 prototype.constructor 只是批改了这个指针而已,并没有批改真正的构造函数。

可能有的敌人会说我打印 myPuppy2.constructor 也有值啊,那 constructor 是不是也是对象自身的一个属性呢?其实不是的,之所以你能打印出这个值,是因为你打印的时候,发现 myPuppy2 自身并不具备这个属性,又去原型链上找了,找到了 prototype.constructor。咱们能够用hasOwnProperty 看一下就晓得了:

下面咱们其实曾经说分明了 prototype__proto__constructor 几者之间的关系,上面画一张图来更直观的看下:

静态方法

咱们晓得很多面向对象有静态方法这个概念,比方 Java 间接是加一个 static 关键字就能将一个办法定义为静态方法。JS 中定义一个静态方法更简略,间接将它作为类函数的属性就行:

Puppy.statciFunc = function() {    // statciFunc 就是一个静态方法
  console.log('我是静态方法,this 拿不到实例对象');
}      

Puppy.statciFunc();            // 间接通过类名调用

静态方法和实例办法最次要的区别就是实例办法能够拜访到实例,能够对实例进行操作,而静态方法个别用于跟实例无关的操作。这两种办法在 jQuery 中有大量利用,在 jQuery 中 $(selector) 其实拿到的就是实例对象,通过 $(selector) 进行操作的办法就是实例办法。比方 $(selector).append(),这会往这个实例 DOM 增加新元素,他须要这个 DOM 实例才晓得怎么操作,将append 作为一个实例办法,他外面的 this 就会指向这个实例,就能够通过 this 操作 DOM 实例。那什么办法适宜作为静态方法呢?比方 $.ajax,这里的ajax 跟 DOM 实例没关系,不须要这个 this,能够间接挂载在 $ 上作为静态方法。

继承

面向对象怎么能没有继承呢,依据后面所讲的常识,咱们其实曾经可能本人写一个继承了。所谓继承不就是子类可能继承父类的属性和办法吗?换句话说就是子类可能找到父类的 prototype,最简略的办法就是子类原型的__proto__ 指向父类原型就行了。

function Parent() {}
function Child() {}

Child.prototype.__proto__ = Parent.prototype;

const obj = new Child();
console.log(obj instanceof Child);   // true
console.log(obj instanceof Parent);   // true

上述继承办法只是让 Child 拜访到了 Parent 原型链,然而没有执行 Parent 的构造函数:

function Parent() {this.parentAge = 50;}
function Child() {}

Child.prototype.__proto__ = Parent.prototype;

const obj = new Child();
console.log(obj.parentAge);    // undefined

为了解决这个问题,咱们不能单纯的批改 Child.prototype.__proto__ 指向,还须要用 new 执行下 Parent 的构造函数:

function Parent() {this.parentAge = 50;}
function Child() {}

Child.prototype.__proto__ = new Parent();

const obj = new Child();
console.log(obj.parentAge);    // 50

上述办法会多一个 __proto__ 层级,能够换成批改 Child.prototype 的指向来解决,留神将 Child.prototype.constructor 重置回来:

function Parent() {this.parentAge = 50;}
function Child() {}

Child.prototype = new Parent();
Child.prototype.constructor = Child;      // 留神重置 constructor

const obj = new Child();
console.log(obj.parentAge);    // 50

当然还有很多其余的继承形式,他们的原理都差不多,只是实现形式不一样,外围都是让子类领有父类的办法和属性,感兴趣的敌人能够自行查阅。

本人实现一个 new

联合下面讲的,咱们晓得 new 其实就是生成了一个对象,这个对象可能拜访类的原型,晓得了原理,咱们就能够本人实现一个 new 了。

function myNew(func, ...args) {const obj = {};     // 新建一个空对象
  const result = func.call(obj, ...args);  // 执行构造函数
  obj.__proto__ = func.prototype;    // 设置原型链
  
  // 留神如果原构造函数有 Object 类型的返回值,包含 Functoin, Array, Date, RegExg, Error
  // 那么应该返回这个返回值
  const isObject = typeof result === 'object' && result !== null;
  const isFunction = typeof result === 'function';
  if(isObject || isFunction) {return result;}
  
  // 原构造函数没有 Object 类型的返回值,返回咱们的新对象
  return obj;
}

function Puppy(age) {this.puppyAge = age;}

Puppy.prototype.say = function() {console.log("汪汪汪");
}

const myPuppy3 = myNew(Puppy, 2);

console.log(myPuppy3.puppyAge);  // 2
console.log(myPuppy3.say());     // 汪汪汪

本人实现一个 instanceof

晓得了原理,其实咱们也晓得了 instanceof 是干啥的。instanceof 不就是查看一个对象是不是某个类的实例吗?换句话说就是查看一个对象的的原型链上有没有这个类的prototype,晓得了这个咱们就能够本人实现一个了:

function myInstanceof(targetObj, targetClass) {
  // 参数查看
  if(!targetObj || !targetClass || !targetObj.__proto__ || !targetClass.prototype){return false;}
  
  let current = targetObj;
  
  while(current) {   // 始终往原型链下面找
    if(current.__proto__ === targetClass.prototype) {return true;    // 找到了返回 true}
    
    current = current.__proto__;
  }
  
  return false;     // 没找到返回 false
}

// 用咱们后面的继承试验下
function Parent() {}
function Child() {}

Child.prototype.__proto__ = Parent.prototype;

const obj = new Child();
console.log(myInstanceof(obj, Child) );   // true
console.log(myInstanceof(obj, Parent) );   // true
console.log(myInstanceof({}, Parent) );   // false

ES6 的 class

最初还是提一嘴 ES6 的 class,其实 ES6 的 class 就是后面说的函数类的语法糖,比方咱们的 Puppy 用 ES6 的 class 写就是这样:

class Puppy {
  // 构造函数
  constructor(age) {this.puppyAge = age;}
  
  // 实例办法
  say() {console.log("汪汪汪")
  }
  
  // 静态方法
  static statciFunc() {console.log('我是静态方法,this 拿不到实例对象');
  }
}

const myPuppy = new Puppy(2);
console.log(myPuppy.puppyAge);    // 2
console.log(myPuppy.say());       // 汪汪汪
console.log(Puppy.statciFunc());  // 我是静态方法,this 拿不到实例对象

应用 class 能够让咱们的代码看起来更像规范的面向对象,构造函数,实例办法,静态方法都有明确的标识。然而他实质只是扭转了一种写法,所以能够看做是一种语法糖,如果你去看 babel 编译后的代码,你会发现他其实也是把 class 编译成了咱们后面的函数类,extends 关键字也是应用咱们后面的原型继承的形式实现的。

总结

最初来个总结,其实后面大节的题目就是外围了,咱们再来总结下:

  1. JS 中的函数能够作为函数应用,也能够作为类应用
  2. 作为类应用的函数实例化时须要应用 new
  3. 为了让函数具备类的性能,函数都具备 prototype 属性。
  4. 为了让实例化进去的对象可能拜访到 prototype 上的属性和办法,实例对象的 __proto__ 指向了类的 prototype。所以prototype 是函数的属性,不是对象的。对象领有的是 __proto__,是用来查找prototype 的。
  5. prototype.constructor指向的是构造函数,也就是类函数自身。扭转这个指针并不能扭转构造函数。
  6. 对象自身并没有 constructor 属性,你拜访到的是原型链上的prototype.constructor
  7. 函数自身也是对象,也具备 __proto__,他指向的是 JS 内置对象Function 的原型 Function.prototype。所以你能力调用func.call,func.apply 这些办法,你调用的其实是 Function.prototype.callFunction.prototype.apply
  8. prototype自身也是对象,所以他也有 __proto__,指向了他父级的prototype__proto__prototype的这种链式指向形成了 JS 的原型链。原型链的最终指向是 Object 的原型。Object下面原型链是 null,即Object.prototype.__proto__ === null
  9. 另外要留神的是 Function.__proto__ === Function.prototype,这是因为 JS 中所有函数的原型都是Function.prototype,也就是说所有函数都是Function 的实例。Function自身也是能够作为函数应用的 —-Function(),所以他也是 Function 的一个实例。相似的还有 ObjectArray 等,他们也能够作为函数应用:Object(), Array()。所以他们自身的原型也是Function.prototype,即Object.__proto__ === Function.prototype。换句话说,这些能够 new 的内置对象其实都是一个类,就像咱们的 Puppy 类一样。
  10. ES6 的 class 其实是函数类的一种语法糖,书写起来更清晰,但原理是一样的。

再来看一下残缺图:

文章的最初,感激你破费贵重的工夫浏览本文,如果本文给了你一点点帮忙或者启发,请不要悭吝你的赞和 GitHub 小星星,你的反对是作者继续创作的能源。

作者博文 GitHub 我的项目地址:https://github.com/dennis-jiang/Front-End-Knowledges

退出移动版