【前端面试题】JS中__proto__和prototype区别与关系

基础知识

首先,要明确几个点:

  1. 在 JS 里,万物皆对象。

所有的 JS 对象都会从一个 prototype(原型对象)中继承属性和方法。

方法(Function)是对象,方法的原型(Function.prototype)是对象。因此,它们都会具有对象共有的特点。

即:对象具有属性__proto__,可称为隐式原型,一个对象的隐式原型指向构造该对象的构造函数的原型,这也保证了实例能够访问在构造函数原型中定义的属性和方法。

  1. 方法(Function)

方法这个特殊的对象,除了和其他对象一样有上述__proto__属性之外,还有自己特有的属性——原型属性 (prototype),这个属性是一个指针,指向一个对象,这个对象的用途就是包含所有实例共享的属性和方法(我们把这个对象叫做原型对象)。原型对象也有一个属性,叫做 constructor,这个属性包含了一个指针,指回原构造函数。

小结:

  • 所有对象都具有__proto__ 的隐式原型。 指向构造该对象的构造函数的原型(prototype)。 (解决我是哪里来的问题,实现方法的继承)

  • 所有的Function 是对象,并且拥有特殊的—原型属性 (prototype)。 指针,指向一个对象(原型对象)。此对象包含所有实例共享的属性和方法。 同时还具有constructor:指针 指回原构造函数。 (解决继承问题,并且得知道我的父亲是谁。因此只有方法具有)

知道了这两个基本点,我们来看看上面这幅图。

  1. 构造函数Foo()
    构造函数的原型属性Foo.prototype指向了原型对象,在原型对象里有共有的方法,所有构造函数声明的实例(这里是 f1,f2)都可以共享这个方法。

  2. 原型对象Foo.prototype
    Foo.prototype保存着实例共享的方法,有一个指针constructor指回构造函数。

  3. 实例
    f1 和 f2 是 Foo 这个对象的两个实例,这两个对象也有属性__proto__,指向构造函数的原型对象,这样子就可以像上面 1 所说的访问原型对象的所有方法啦。

另外:

  1. 构造函数Foo()除了是方法,也是对象,它也有__proto__属性,指向谁呢?
    指向它的构造函数的原型对象。函数的构造函数不就是 Function 嘛,因此这里的__proto__指向了Function.prototype
    其实除了Foo()Function(), Object()也是一样的道理。
  2. 原型对象也是对象,它的__proto__属性,又指向谁呢?
    同理,指向它的构造函数的原型对象。这里是Object.prototype.
    最后,Object.prototype 的__proto__属性指向 null。

总结:

  1. 对象有属性__proto__, 指向该对象的构造函数的原型对象。
  2. 方法除了有属性__proto__,还有属性prototypeprototype指向该方法的原型对象。

隐式原型与显式原型

__proto__(隐式原型)
prototype(显式原型)

1.是什么

显式原型 explicit prototype property:
每一个函数在创建之后都会拥有一个名为prototype的属性,这个属性指向函数的原型对象。
Note:通过Function.prototype.bind方法构造出来的函数是个例外,它没有prototype属性。

隐式原型 implicit prototype link:
JavaScript 中任意对象都有一个内置属性[[prototype]],在 ES5 之前没有标准的方法访问这个内置属性,但是大多数浏览器都支持通过__proto__来访问。
ES5 中有了对于这个内置属性标准的 Get 方法Object.getPrototypeOf().
Note: Object.prototype 这个对象是个例外,它的__proto__值为 null

二者的关系:
隐式原型指向创建这个对象的函数(constructor)的 prototype

2. 作用是什么

  • 显式原型的作用:用来实现基于原型的继承与属性的共享。

  • 隐式原型的作用:构成原型链,同样用于实现基于原型的继承。举个例子,当我们访问 obj 这个对象中的 x 属性时,如果在 obj 中找不到,那么就会沿着__proto__依次查找。

3.[[proto]]指向

__proto__的指向到底如何判断呢?
根据 ECMA 定义 'to the value of its constructor’s "prototype" ' ----指向创建这个对象的函数的显式原型。
所以关键的点在于找到创建这个对象的构造函数,接下来就来看一下 JS 中对象被创建的方式,主要有三种方式:

  1. 对象字面量的方式
  2. new 的方式
  3. ES5 中的 Object.create() 但是我认为本质上只有一种方式,也就是通过 new 来创建。
    为什么这么说呢,首先字面量的方式是一种为了开发人员更方便创建对象的一个语法糖,
    本质就是
const r o = new Object();
o.xx = xx;
o.yy=yy;

再来看看 Object.create(),这是 ES5 中新增的方法,在这之前这被称为原型式继承。

构造函数的显示原型的隐式原型:
内建对象(built-in object):比如Array()Array.prototype.__proto__指向什么?
Array.prototype也是一个对象,对象就是由 Object() 这个构造函数创建的,因此
Array.prototype.__proto__ === Object.prototype //true,或者也可以这么理解,所有的内建对象都是由 Object()创建而来。

  1. 自定义对象
function Foo() {}
var foo = new Foo();
foo.__proto__ === Foo.prototype; //true
foo.prototype === undefined; //true
Foo.prototype.__proto__ === Object.prototype; //true 理由同上
Object.prototype.__proto__ === null; //true
Foo.prototype.__proto__.__proto__ === null; //true
  1. 其他情况:
function Bar() {} //这时我们想让Foo继承Bar, 注意这个继承的方法 所引起的 指针指向。
Foo.prototype = new Bar();
Foo.prototype.__proto__ === Bar.prototype; //true
//我们不想让Foo继承谁,但是我们要自己重新定义Foo.prototype
Foo.prototype = {
  a: 10,
  b: -10,
};
//这种方式就是用了对象字面量的方式来创建一个对象,根据前文所述
Foo.prototype.__proto__ === Object.prototype; //false

注: 以上两种情况都等于完全重写了Foo.prototype,所以Foo.prototype.constructor也跟着改变了。
于是constructor这个属性和原来的构造函数Foo()也就切断了联系。
构造函数的隐式原型
既然是构造函数那么它就是Function()的实例,因此也就指向Function.prototype,比如 Object.__proto__ === Function.prototype

4. instanceof

instanceof 操作符的内部实现机制和隐式原型、显式原型有直接的关系。
instanceof的左值一般是一个对象,右值一般是一个构造函数,用来判断左值是否是右值的实例。它的内部实现原理是这样的:
//设 L instanceof R
//通过判断
L.__proto__.__proto__ ..... === R.prototype ?
//最终返回 true or false
也就是沿着 L 的proto一直寻找到原型链末端,直到等于 R.prototype 为止。知道了这个也就知道为什么以下这些奇怪的表达式为什么会得到相应的值了

 Function instanceof Object // true
 Object instanceof Function // true
 Function instanceof Function //true
 Object instanceof Object // true
 Number instanceof Number //false

常见范例

范例 1: 有趣的继承关系

使用 Object.create()方式实现继承

function Point() {}

var Circle = Object.create(Point); ///这个地方使用的es6的方法,看这个地方用的就不对,相当忽悠应该用 Object.create(Point.prototype);
console.log(Circle.__proto__ === Point); // true
console.log(Circle.__proto__ === Point.prototype); // false ,变为true的条件是 Object.create(Point.prototype);
console.log(Circle.__proto__);
console.log(Point.prototype);

//Circle.__proto__ === Point.prototype  结果 false的问题 说明。
// Object.create = function(p) {
//     function f(){}
//     f.prototype = p;
//     return new f();
// }

使用 new 方式实现继承

//---------------使用new方法进行实例化
var p = new Point();
console.log(Point.__proto__); // ƒ () { [native code] }  是native的代码实现的 built-in 函数,而不是JavaScript代码。
console.log(Point.prototype); // Point {}
console.log(p.__proto__); // Point {}
console.log(Point.prototype === p.__proto__); //true
console.log(p.prototype); // undefined
//----------------------------------测试new 和 Object.create() 两个的区别  测试案例 在浏览器中使用
function Person(name, sex) {
  this.name = name;
  this.sex = sex;
}
Person.prototype.getInfo = function () {
  console.log("getInfo: [name:" + this.name + ", sex:" + this.sex + "]");
};

console.log(Person);
//  ƒ Person(name, sex) {
//     this.name = name;
//     this.sex = sex;
//  }

console.log(Person.prototype);
//输出的结果是如下的三个对象
//getInfo: ƒ ()  这个容易理解是我们定的函数
//constructor: ƒ Person(name, sex)  注意这个里面就很神奇了。他指向了Person,就这样他们就一直转圈圈了。
//__proto__: Object

console.log(Person.prototype.constructor === Person); //true
//因此出现了上面神一样的一幕。

var a = new Person("jojo", "femal");
console.log(a);
//输出的结果
//name: "jojo"
//sex: "femal"
//__proto__: Object   这里__proto__ 是一个对象指向了 Person.prototype ,
//因此 a.__proto__ === Person.prototype  是true 没问题
///!!!!! 因此这里 使用a.getInfo 是可以正确输出的,因为他会沿着__proto__向上查找,找到 原型中的getInfo 方法。

//紧接这我们创建b对象
var b = Object.create(Person);
console.log(a === b); //返回结果为false,为什么呢,我们探讨下。
console.log(b);
//Function(){} 但是其 __proto__ : ƒ Person(name, sex)
//仔细看下 这种写法是不怎么对的,因为Person是一个function 并非Object。因此还是不太对

//现在我们明白了,那我们
var c = Object.create(Person.prototype);
console.log(c); //Person {} Person 的 __proto__执行了 Person.prototype
console.log(c.__proto__ === Person.prototype); //返回结果为true, 这样返回结果就对了

console.log(a === c); //false , c它就是生成了一个实例,这个实例的原型由我来指定,但是它的构造函数F被隐藏了。
也就是构造函数被隐藏了;
console.log(a.__proto__ === c.__proto__); //ture 他们两个原型链是一样的 但是a 比 c 多了构造函数内部的数据
console.log(a.name, c.name);
"jojo", undefined; //印证了上面的道理
console.log(a.getInfo(), c.getInfo()); //两者也都有 印证了上面的方法

//通过Object.create()生成的实例怎么才能有自己的属性呢,别怕,我们看一下它完成的面目是怎样的,
//Object.create(proto, [propertiesObject]), 人家是有第二个参数滴。可以自己定义它的属性,可写、可配、可枚举,均可自己定义,实现一条龙自助服务。

范例 2: 实现继承

function Student(name, sex, age) {
  Person.call(this, name, sex);
  this.age = age;
}
//原型链拼接
Student.prototype = Object.create(Person.prototype);
//构造函数弯了,纠正
Student.prototype.constructor = Student;
Student.prototype.getInfo = function () {
  console.log(
    "getInfo: [name:" +
      this.name +
      ", sex:" +
      this.sex +
      ", age:" +
      this.age +
      "]."
  );
};
var s = new Student("coco", "femal", 25);

sStudent的实例,Person.prototype在 s 的原型链上,并且这个 F 实例的 constructor 又指回了 Student。既然说到了这里,那如果想要一个对象继承多个类怎么办呢?在上面的代码基础上使用 mixin 加工下就可啦。

//再创建一个基类
function Animal(age) {
  this.age = age;
}
Animal.prototype.say = function (language) {
  console.log("you say " + language);
};

function Student(name, sex, age) {
  Person.call(this, name, sex);
  Animal.call(this, age);
}
//原型链拼接
Student.prototype = Object.create(Person.prototype);
Object.assign(Student.prototype, Animal.prototype);
Student.prototype.constructor = Student;
Student.prototype.getInfo = function () {
  console.log(
    "getInfo: [name:" +
      this.name +
      ", sex:" +
      this.sex +
      ", age:" +
      this.age +
      "]."
  );
};
var s = new Student("coco", "femal", 25);

s 的原型仍是一个 F 实例,其原型指向了Person,但是这个 F 实例上 mixin 了 Animal 的方法。但是原型链上并没有 Animal 什么事,通过 instanceof 可以验证这一点。这种方式比较适合父类为同级别,子类只要拥有相同的属性和方法即可的情况。

关于 object.create 简化版

Object.create = function (obj) {
  function F() {}
  F.prototype = obj;
  return new F();
};

上面代码表明,Object.create 方法的实质是新建一个空的构造函数 F,然后让 F.prototype 属性指向参数对象 obj,最后返回一个 F 的实例,从而实现让该实例继承 obj 的属性。

关于我
loading