浅谈prototype、__proto__、原型链的关系和区别

2,315 阅读7分钟

(本文只是个人的学习记录,请大佬指出错误。😀)

在了解这几项的关系和区别之前,我们需要先搞懂这三项分别是什么、用处是什么,在哪边用,才能更好地帮助我们深入了解其关系。

1、prototype(原型)

Javascript中对象的 prototype 属性的解释是:每个函数都有一个特殊的属性叫作原型(prototype)。 这边其实是比较抽象的,我们可以这样理解,Javascript 是面向对象的,每个函数都有对象,每个对象都有一个 prototype 属性,该属性是一个指针,指向一个对象(构造函数的原型对象);同时这些对象都有一个 constructor 属性,这个属性指向所关联的构造函数(可以理解为函数的来源)。

原型对象还有一个方面就是利用它来实现属性和方法的继承, 比如定义在 prototype 属性之上的——那些以 Object.prototype. 开头的属性,同时不仅仅以 Object. 开头的属性。于是Object.prototype.toString(),Object.prototype.valueOf()  等等,可以用于任何继承自 Object() 的对象类型,包括使用构造器创建的新的对象实例。

原型对象的出现就是可以让所有实例对象共享它所拥有的属性和方法。但需要注意的是,这个属性只在构造函数或类中才出现。

prototype属性的两个例子(还是看代码好理解一点🙈)

1、用prototype给对象添加属性或函数

var person1=new Person("zhangsan");

function Person(){} //创建一个构造函数
Person.prototype.name = "xxx";//在构造函数的原型上创建一个name属性
Person.prototype.say = function(){//在构造函数的原型上创建一个say方法
    console.log("hello");
}
var person = new Person();//创建一个实例对象的同时,设置实例对象的__proto__属性(原型链也就产生了)
console.log(person.name);// xxx
person.say();// hello

2、实现继承来减少的重复代码

function Person(){
    this.name = 'xxx'
} //创建一个构造函数Person
function Teacher(){} //创建另外一个构造函数Teacher
Teacher.prototype = new Person() //也就是Teacher的原型将继承Person上所有的属性
var obj = new Teacher();
console.log(obj.name);// xxx

2、proto(隐式原型)

每个实例对象(object)都有一个私有属性(称之为 __proto__ )指向它的构造函数的原型对象prototype。该原型对象也有一个自己的原型对象(__proto__),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

proto的定义过程:proto属性是在调用构造函数创建实例对象时产生的,这时因为当一个对象被创建时,这个构造函数将会把它的属性 prototype 赋给新对象的内部属性__proto__,于是这个__proto__被这个实例对象用来查找创建它的构造函数的prototype属性。

注意,该属性存在于实例和构造函数的原型对象之间,而不是存在于实例与构造函数之间。

Proto与prototype的关系

先定义一个基础函数 Person

function Person(name){
    this.name = name;
    this.sayName = function(){
        console.log(this.name);
    };
}
var person1=new Person("zhangsan");

1. __proto__与 prototype的基础关系

console.log(person1.__proto__); // {constructor: ƒ Person(name);[[Prototype]]: Object} 
// person1 实例对象的__proto__指向 Person 的原型
console.log(Person.prototype); // {constructor: ƒ Person(name);[[Prototype]]: Object}
// Person 的原型,与上面相等
console.log(person1.prototype) // undefined
// 实例对象是不存在原型的

证实可以得到 person1.__proto__ = Person.prototyp 并且,实例对象是不存在 prototype 属性的。

console.log(Person.prototype.constructor === Person) // true

一个构造函数的原型里的 constructor 属性指向这个构造函数本身。

2. Function.prototype

每个 JavaScript 函数实际上都是一个 Function 对象。运行 (function(){}).constructor === Function // true 便可以得到这个结论。在我们代码中的构造函数、内置对象都是由 Function 创建的,通过 new 调用可以生成函数对象,比如自己创建的Person构造函数,以及Number、String、Boolean、Object、Error、Array、RegExp、Date、Function等内部对象。

举例:Number 引用类型(proto),其实就是 ƒ Number() { [native code] };

既然如此,这些函数的 proto 属性,指向的就是 Function.prototype。所以 Fucntion 这个函数的prototype 是所有函数的 proto ,有 call, apply等方法。

console.log(Person.__proto__ === Function.prototype) //true 
// Person 函数是由Function定义来的,所以它的__proto__就指向Function的原型
console.log(Function.prototype.apply.__proto__ === Function.prototype) // true
// Function.prototype有函数的apply 方法,其本身也是个函数,它的__proto__同样指向Function的原型
console.log(Number.__proto__ === Function.prototype) // true

下面引入一张图,解释下__proto__的指向

image.png

3. Object.prototype.proto

Object 是原型链的最后指向, 因为 Object.prototype.__proto__ 如果指回自己的话,那原型链的话就是一条死链了。所以 Object.prototype.__proto__为 null,当查找到该属性 null 的时候就可以停止原型链的搜索。

console.log(Object.prototype.__proto__) // null

我们看个数组的原型链的过程,最里面一层是 Object.prototype

console.log([].__proto__.__proto__ === Array.prototype.__proto__ === Object.prototype) // true
// [] 的构造原型是 Array.prototype, Array.prototype 的构造原型是 Object.prototype, 
// Object.prototype 的__proto__属性是 null

再举个例子,我们这边创建的构造函数 Person,它的 prototype 属性就是有 Object 创建的,所以 Object 将自己的 prototype 属性,扔给了 Person 的原型的 __proto__ 属性。

console.log(Person.prototype.__proto__ === Object.prototype) // true

那么,此时有个疑问,Function.prototype指的是什么,我们可以看到一个打印结果 Function.prototype = fuction(){[Native code]},其实可以将fuction(){}可以看成对象 Object 的实例,下面就跟上面的是一样的了。

console.log(Function.prototype.__proto__ === Object.prototype) // true

4.补充:因为根据官方的建议最好不使用内部属性 proto ,我们一般用 getPrototypeOf 代替

使用__proto__是有争议的,也不鼓励使用它。因为它从来没有被包括在EcmaScript语言规范中,但是现代浏览器都实现了它。__proto__属性已在ECMAScript 6语言规范中标准化,用于确保Web浏览器的兼容性,因此它未来将被支持。它已被不推荐使用, 现在更推荐使用Object.getPrototypeOf/Reflect.getPrototypeOf 和Object.setPrototypeOf/Reflect.setPrototypeOf(尽管如此,设置对象的[[Prototype]]是一个缓慢的操作,如果性能是一个问题,应该避免)。

  我们可以用 getPrototypeOf 方法获取对象的隐式原型

var obj={};
var arr=[];
console.log(Object.getPrototypeOf(obj)===Array.prototype)//false
console.log(Object.getPrototypeOf(arr)===Array.prototype)//true

3、原型链

基本原理是: 当我们在使用原型时,一般会利用原型让一个函数继承另一个函数的属性和方法, 直观的表现就是让一个函数的原型指向另一个类型的实例;当访问实例对象的某个属性时,会先在这个对象本身的属性上查找,如果没有找到,则会通过__proto__属性去原型上找; 如果还没有找到,则会在构造函数的原型的__proto__中去找,这样一层层向上查找就会形成一个作用域链,称为原型链。

搜索过程总是要一环一环地前行到原型链末端才会停下来.

继续以上面 var p1 = new Person() 函数为例子,通过原型链一层层去找一个属性。

  • 会先在这个对象p1本身的属性上查找,没有的话
  • p1.__proto__ ,也就是 Person.prototype的属性去寻找,没有的话
  • 继续找 Person.prototype 里面的 __proto__ 的属性,这边就是 Object.prototype ,其中还是没有
  • 继续找 Object.prototype.__proto__ ,发现是null,则停止寻找,一次原型链查找操作完成

4、原型的应用

1、伪数组转换成真数组,以原型方式调用对象的方法

Array.prototype.slice.call(伪数组) 
// 比如 document.getElementsByClassName 的返回值
Number.prototype.toFixed.call(1.123,2) // 1.12
。。。

2、用 constructor,instanceof,object.prototype.toString 方法通过对象的构造函数判断

var arr1 = [];
console.log(arr1.constructor === Array)//false
var arr2 = [];
console.log(arr2 instanceof Array); // true
console.log(Object.prototype.toString.call([1,2,3]) === '[object Array]');//true

3、Object.prototype.hasOwnProperty(属性名):判断某个对象属性名是不是对象自身的

var ob = new Object()
ob.name = 'xxx'
ob.hasOwnProperty('name') // true

4、Object.Create(对象) :创建一个空对象,并且将对象的隐式原型修改为指定的参数,不能为null

var a = {a: 1};
var b = Object.create(a);
console.log(b.__proto__)  // {a: 1}

5、利用原型实现一个 instanceOf 方法

function myInstanceof(left, right) {
    // 这里先用typeof来判断基础数据类型,如果是,直接返回false
    if(typeof left !== 'object' || left === null) return false;
    // getProtypeOf是Object对象自带的API,能够拿到参数的原型对象
    let proto = Object.getPrototypeOf(left);
    while(true) {
        if(proto === null) return false;
        if(proto === right.prototype) return true;//找到相同原型对象,返回true
        proto = Object.getPrototypeof(proto);
    }
}

5、总结

不断积累,不断沉淀,不断刷新,学习就是这么一个不断更新的过程。也许学习的结果无法在短期预见,但不积硅步,又如何能至千里呢?当我们面临难题却能坦然且有效地找到方式解决,方是我们做这一切的意义所在。就如同这章学习的内容,看上去都是理论知识,没啥实践,但其中一些细节却对我们的工作起到很大的帮助。