你还没学会Javascript原型和原型链吗?

【线上直播】11月21日晚8点贝壳技术总监侯圣文《数据安全之数据库安全黄金法则》

 你还没学会Javascript原型和原型链吗?

前言

在前端这块领域,原型与原型链是每一个前端er必须掌握的概念。我们多次在面试或者一些技术博客里面看见这个概念。由此可见,这个玩意对于前端来说有多重要。其实它本身理解起来不难,但是很多刚入行前端的同学,看到prototype、__proto__理解起来还是有点吃力,然后脑子里面就乱成一锅粥,就像我一样。但是这是很正常的事情,没什么大不了的,就像我们想要学会跑步,那么我们就必须先学会走路。任何事情都是有个过程的。所以现在就跟我一起来攻克这个难点吧。通过这篇文章你将掌握以下知识点:

  • 理解 __proto_;
  • 理解 prototype;
  • 理解javascript中对象的概念;
  • 理解原型和原型链;
  • 理解javascript中类的概念;
  • 理解new的实现;
  • 理解instanceof的实现;
  • 理解javascript的继承;
  • 加深对javascript这门语言的理解。

这也是本篇文章的写作思路。

对象

那么我们就从对象这一概念开始说起,其实对象这一概念相信大家并不陌生。有一种说法是“javasrcript中万物皆是对象”,其实这个说法是错误的,一个很简单的例子,javasript中简单基本类型(string、boolean、number、null、undefined、symbol)本身就不是对象。其实javasript中对象主要分为函数对象和普通对象。其中:

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

这些都是函数对象,他们同时也被称为内置对象。函数对象本身其实就是一个纯函数,javascript用他们来模拟类。普通对象就很简单了,就是我们常见的对象:

  1. const obj = { 
  2.     name'juefei'
  3.     desc'cool' 

可能说到这,你还是无法理解到底啥是函数对象,啥是普通对象,那我们就一起来看看下面的代码:

  1. const obj1 = {}; 
  2. const obj2 = new Object(); 
  3. function func1() { 
  4.  
  5. const obj3 = new func1(); 
  6. const func2 = new function() { 
  7.  
  8. const func3 = new Function() 

接着我们来分别打印一下他们:

  1. console.log(obj1);  // object 
  2. console.log(obj2);  // object 
  3. console.log(obj3);  // object 
  4. console.log(func1);  // function 
  5. console.log(func2);  // function 
  6. console.log(func3);  // function 

所以可以看见,obj1、obj2、,obj3是普通对象,他们都是Object的实例,而func1、func2、func3则都是Function的实例,称为函数对象。我们再看看:

  1. console.log(typeof Object);  //f unction 
  2. console.log(typeof Function); // function 

你是不是惊呆了,原来Object和Function都是 Function的实例。所以我们得出一个结论就是:

  • 只要是Function的实例,那就是函数对象,其余则为普通对象。

同样我们也可以看出,不仅 Object 是函数对象,就连 Function 本身也是函数对象,因为我们通过 console.log(typeof Function); 得知 Function 是 Function 的实例。是不是又开始有点绕了?没事,到这一步你就记住我们刚刚的结论就算完成目标:

  • 只要是Function的实例,那就是函数对象,其余则为普通对象。

那么说到对象,我们从上面可以看出,一个对象是通过构造函数 new 出来的,这其实跟原型和原型链有很大的关系,那么原型和原型链到底是用来干嘛的呢?

原型

涉及到这两个概念,我们就必须先来介绍两个东西: __proto__ 和 prototype ,这两个变量可以说,在 javascript 这门语言里面随处可见,我们不管他三七二十一,我们先来看一张表:

你还没学会Javascript原型和原型链吗?

所以,请你先记住以下结论:

  • 只有函数对象有 prototype 属性,普通对象 没有这个属性。
  • 函数对象 和 普通对象 都有 __proto__这个属性。
  • prototype 和 __proto__都是在创建一个函数或者对象会自动生成的属性。

接着我们来验证一下:

  1. function func (){  //func称为构造函数 
  2.  
  3. console.log( typeof func.prototype); // object 
  4. console.log(typeof func.__proto__);  // function 
  1. const obj = {} 
  2. console.log(typeof obj.__proto__) //object 
  3. console.log(typeof obj.prototype) //undefined (看见了吧,普通对象真的没有 prototype 属性) 

所以就验证了我们刚刚的结论:

  • 只有函数对象有 prototype 属性,普通对象 没有这个属性
  • 函数对象 和 普通对象 都有 __proto__这个属性。
  • prototype 和 __proto__都是在创建一个函数或者对象会自动生成的属性。

你看我又重复写了一遍,我不是为了凑字数,是为了你加深记忆,这对于我们接下来的篇幅很重要。接着我们来看看下面的代码:

  1. console.log(obj.__proto__ === Object.prototype); // true 
  2. console.log(func.__proto__ === Function.prototype); // true 

所以我们又得出如下结论:

  • 实例的 __proto__属性主动指向构造的 prototype;
  • prototype 属性被 __proto__ 属性 所指向。

这就是prototype 属性和 __proto__ 属性的区别与联系。这可能又有点绕了,来多看几遍这一节,多背一下我们的结论。我们继续。

那么问题来了,既然func是一个函数对象,函数对象是有 prototype 属性的,那么func.prototype.__proto__等于啥呢?

为了解决这个问题,我们来思考一下:

首先,我们看看func.prototype 是啥:

  1. console.log(typeof func.prototype); //object 

好,我们知道了,func.prototype 是一个对象,那既然是对象,那 func.prototype 那不就是 Object的实例吗?那也就是说,func.prototype.__proto__属性肯定是指向 Object.prototype 咯!好,我们来验证一下:

  1. console.log(func.prototype.__proto__ === Object.prototype); //true 

看见没有,就是这样的。那看到这里,我们应该也知道当我们这创建一个构造函数的时候,javascript是如何帮我们自动生成__proto__和prototype属性的。哈哈没错就是这样:

  1. //我们手动创建func函数 
  2. function func() {} 
  3. //javascript悄悄咪咪执行以下代码: 
  4. func._proto = Function.prototype; //实例的 __proto__ 属性主动指向构造的 prototype 
  5. func.prototype = { 
  6.     constructor: func, 
  7.     __proto: Object.prototype //我们刚刚才在上面验证的,你别又忘记了 

我还专门为你画了个图(够贴心不老铁):

 你还没学会Javascript原型和原型链吗?

所以prototype又被称为显式原型对象,而__proto__又被称为隐式原型对象。

hi,看到这里,你是不是有种脑子开了光的感觉。哈哈,所以到现在你应该已经理解原型的概念了,如果你还不理解,那就把上述章节再看一遍。最好拿个纸笔出来跟着画一画,顺便拿出电脑把示例代码敲一敲。好,整理一下头脑,接下来我们来看看什么又是原型链。

原型链

再介绍这个概念之前,我们先来看如下代码:

  1. function Person = function(name,desc){ 
  2.     this.name = name
  3.     this.desc = desc
  4. } //***1****// 
  5. Person.prototype.getName = function(){ 
  6.     return this.name
  7. }//***2****// 
  8. Person.prototype.getDesc = function(){ 
  9.     return this.desc
  10. }//***3****// 
  11.  
  12. const obj = new Person('juefei','cool');//***4****// 
  13. console.log(obj);//***5****// 
  14. console.log(obj.getName);//***6****// 

接下来我们来逐步解析一下:

1.创建了一个构造函数 Person,此时,Person.portotype自动创建,其中包含了 constructor 和 __proto__两个属性;

2.给对象 Person.prototype 新增了一个方法 getName;

3.给对象 Person.prototype 新增了一个方法 getDesc;

4.构造函数 Person 新建一个实例: obj(在创建实例的时候,构造函数会自动执行);

5.打印实例 obj :

  1.     name'juefei'
  2.     desc'cool' 

根据上面一节的结论,我们得出:

  1. obj.__proto__ = Person.prototype; 

6.执行到第6步时,由于在实例 obj 上面找不到 getName()这个方法,所以它就会自动去通过自身的 __proto__ 继续向上查找,结果找到了 Person.prototype ,接着它发现,刚好 Person.prototype 上面有getName()方法,于是找到了这个方法,它就停止了寻找。怎么样,是不是有一种环环相扣的感觉?他们形成一个链了,没错,这就是原型链。

我们得出如下结论:

在访问一个对象(假设这个对象叫obj)的属性/方法时,若在当前的对象上面找不到,则会尝试通过obj.__proto__去寻找,而 obj.__proto__ 又指向其构造函数(假设叫objCreated)的 prototype,所以它又自动去 objCreated.prototype 的属性/方法上面去找,结果还是没找到,那么就访问 objCreated.prototype.__proto__继续往上面寻找,直到找到,则停止对原型链对寻找,若最终还是没能找到,则返回 undefined 。一直沿着原型链寻找下去,直到找到 Object.prototype.__proto__,指向 null,于是返回 undefined了。

是不是自然而然就理解了。我又给你画了个图(请对照着上面