一篇文章带你理解prototype 原型

959 阅读6分钟
文章的开头,我们先来讲一个故事:

村庄里有一个大池塘,池塘里有很多鱼。池塘是我们大家的,所以里面的鱼我们都可以吃。

我们也会从集市买一些鱼放在家里,那么家里的鱼肯定是属于我们私人的,外人是不会拥有的。

那么在js里我们就把这个池塘称为原型对象,池塘里面我们所共享的鱼称为原型中的属性及方法,而我们自己的鱼称为构造函数中的属性及方法,而我们是对象的实例

以上是为了让大家能够趣味性的对prototype有一个概念,接下来就通过代码具体总结一下prototype~

一、理解prototype

我们创建的每一个函数都有一个prototype属性,这个属性是一个指向对象的指针

构建对象中有一种模式叫做原型模式,意思是将对象实例所不可共享的属性及方法定义在构造函数中,而将可共享的属性及方法放在原型对象中,也就是prototype指向的对象中。以下是用原型模式创建的一个对象

 function person(name, age) {
   this.name = name;
    this.age = age;
  }
  person.prototype = {
    sayName: function() {
      console.log(this.name);      
    }
  };
 
 var p1 = new person("Wind", 20);
 p1.sayName(); // "Wind"
 
 var p2 = new person("Nic", 20);
 p2.sayName(); // Nic

这里我将name、age属性定义在构造函数中,将sayName方法定义在原型中。所以p1和p2对象实例的内存空间里面各有一份name和age,但是它们却共享一份sayName方法,意思是它们调用的sayName方法是同一个。

试想如果我们不用prototype,而是直接将sayName写进构造函数呢?

那么p1和p2中将各有一份sayName,这样浪费内存空间,所以用prototype的好处之一:提高了代码的复用性,减少内存。

在了解原型对象的同时我们还有一个小知识要明白:每当代码读取一个对象属性的时候会执行一次搜索,搜索目标是给定名字的属性,搜索路径为:

对象实例本身---->原型对象---->对象所继承的父类对象---->父类对象原型...---->原型链末端

二、prototype的注意点

1、不可变性:尽管prototype是共享的,但能通过对象实例重写原型中的值,但是可以由对象统一改。通俗一点:只能爸爸统一改,不能儿子改。(这也和类型有关系,孩子不能改变基本类型的值,但是可以改变对象,比如数组)

基本类型:

function person() {}
person.prototype = { 
   num: 0
};  
var p1 = new person(); 
var p2 = new person();
p1.num++;
p2.num; // 0

非基本类型:

function person() {
person.prototype = {
   num: [1,2,3]
};  
var p1 = new person(); 
var p2 = new person();
p1.num[2] = 8; 
p2.num; //  [1, 2, 8]  改变了

2、同名覆盖性:如果我们在实例中添加了一个与原型属性同名的属性,那么该属性会创建到对象实例中并且会覆盖掉原型中的相应属性。

function person(name) {
  this.name = name; 
}
person.prototype = {
   age: 18 
};
var p1 = new person("Wind");
var p2 = new person("Nic");
p1.age = 20;
p1.age; // 20
p2.age; // 18

3、使用对象字面量创建原型方法,会切断之前的链而重写原型链

function person(name) {
  this.name = name; 
}
person.prototype = { 
  sayName: function() {
    console.log(this.name);      
  }
};
var p1 = new person("Wind");

person.prototype = {
  age : 20
}; 
p1.sayName();  // Wind
p1.age; // undefined

var p2 = new person("Nic");
p2.sayName();  // error
p2.age;  // 20

因为prototype指针指向了一个新的对象,切断了构造函数与之前的prototype旧对象的联系,然而p1的prototype指针指向的依旧是旧对象。而新创建的实例p2指向的prototype新对象。

三、总结

prototype的用法:构造函数模型用于定义例的属性,而原型模型用于定义方法共享的属性。


面试中的原型和原型链


1 原型链的5条规则

  1. 所有的引用类型(数组,对象,函数),都是具有对象特性的,即可以自由扩展属性(除了null以外)
  2. 所有的引用类型(数组、对象、函数),都有一个proto 属性(隐式原型),这个属性的值是一个普通对象
  3. 所有的函数,都有一个prototype属性(显式原型),这个属性值是一个普通的对象
  4. 所有的引用类型(数组、对象、函数),proto的属性值指向(完全相等)它的构造函数的“prototype”的属性值
  5. 当试图得到一个对象的某一个属性的时候,如果一个对象本身没有这个属性的话,就会去它的proto( 也就是它的构造函数中去寻找这个属性)

2 JS中寻找对象属性的过程

  1. 当一个对象没有这个toString()这个属性的时候,就回去自己的隐式原型proto中去寻找这个属性,也就是去自己额构造函数的显示原型prototype中寻找这个属性(对象自身的隐式原型就是他的构造函数的显式原型)
  2. 发现FOO.prototype中也没有这个toString属性,这也是一个对象,name就去这个对象{}的proto中寻找toString()这个属性

3 instanceof的作用?

是用于判断【引用类型】属于哪个【构造函数】的方法

Note

总结:f.proto 一层一层向上寻找,能否找到FOO.prototype,找到为true,否则为false

4 写一个原型继承的例子

/**
     * 动物类
     * @constructor
     * */
    function Animal (name){
        this.name = name;
        this.eat = function () {
            console.log('My name is ', name, ' I am eating Foods…………');
        }
    }

    /**
     * 小狗类
     * @constructor
     */
    function Dog(){
        this.bark = function () {
            console.log("I am a dog, I am barking……");
        }
    }

    // 如何让这个小狗继承这个Animal的属性呢?
    // 实现思路:每一个函数都有一个prototype属性,这个属性值是一个普通的对象
    Dog.prototype = new Animal();
    var dog = new Dog();
    // 这个小狗有eat() 这个属性吗?发现自身没有,那么就会去dog这个对象的__proto__里面去寻找,也就是他的构造函数Dog的prototype上面去寻找
    // 发现这个对象Dog构造函数的prototype的值是一个对象new Animal(), 这个对象里面是有eat这个属性的,因此就找到了
    dog.eat(); // My name is  undefined  I am eating Foods…………
    console.log(dog.__proto__ === Dog.prototype);       // tru
Copy

5 描述一下new一个对象的过程


  1. 创建一个新对象
  2. this指向这个新对象
  3. 执行代码(对this赋值)
  4. 返回this
  // v1
  function objectFactory() {
      var obj = new Object(),
          // 因为 shift 会修改原数组,所以 arguments 会被去除第一个参数
          Constructor = [].shift.call(arguments);     // 拿到伪数组中的第一个参数
      // 取出参数中的第一个参数,就是我们要传入的构造函数,建立继承关系
      obj.__proto__ = Constructor.prototype;
      Constructor.apply(obj, arguments);
      return obj;
  }

  // v2 : 还需要判断返回的值是不是一个对象,如果是一个对象,我们就返回这个对象,如果没有,我们该返回什么就返回什么。
  function objectFactory() {
      var obj = new Object(),
          Constructor = [].shift.call(arguments);
      // 建立继承关系(二者之间的关系)
      obj.__proto__ = Constructor.prototype;
      // 开始执行这个构造函数
      var ret = Constructor.apply(obj, arguments);
      // 看一下构造函数的返回值,是对象还是一个基本数据类型?
      return typeof ret === 'object' ? ret : obj;
  }


  // v4:Object.create的原理
    // var obj = Object.create(Constructor.prototype);
    // 等价于:
    // var obj = new Object();
    // obj.__proto__ = Constructor.prototype;
    const _new = function () {
        var Constructor = [].shift.call(arguments);
        // 1. 创建一个对象,这个对象要继承与构造函数的原型对象
        var obj = Object.create(Constructor.prototype);
        // 2. 执行这个构造函数
        var ret = Constructor.apply(obj, arguments);
        return typeof ret === 'object' ? ret || obj : obj;
    }

    // v5: 实现一个自己的new构造函数
    const _new = function() {
        // 从Object.prototype上克隆一个对象 
        var obj = new Object();
        // 取出来外部传入的构造器
        var Constructor = [].shift.call(arguments);

        // 使用一个中间的函数来维护原型的关系
        var F = function(){};
        F.prototype = Constructor.prototype;
        obj = new F();

        // 开始执行这个构造函数
        var res = Constructor.apply(obj, arguments);
        // 确保构造器总是返回一个对象(使用res || obj 的方式来防止返回null参数)
        return typeof res === 'object' ? res || obj : obj;
    }