JS 原型链之灵魂拷问

380 阅读8分钟

先附上神图!

你是不是经常被问到下面这几个问题?

  1. 创建对象的有几种方法?
  2. 什么是原型?
  3. 什么是构造函数?
  4. 什么是实例?
  5. 什么是原型链?
  6. instanceof的原理是什么?
  7. new运算符的原理是什么?
  8. 如何实现继承?

刚开始你可能前几个问题还回答的有滋有味,不就是个原型链么,早都熟背于心,倒背如流了。可越来越发现不是这么回事了...

下面我们就来一一解答

1. 创建对象的有几种方法?

一般由三种,分别是:

方法一:使用字面量方式

var obj1 = {name: 'obj'} // 会默认new,和下面obj2的方法效果一样
//或
var obj2 = new Object({name: 'obj'})
输出结果如下:
// {name: "obj"}

方法二:使用显示构造函数创建对象

var Fn = function() {
  this.name='obj'
}
var obj3 = new Fn()
输出结果如下:
// Fn {name: "obj"}

方法三:Object.create

var P = {name: 'obj'}
var obj4 = Object.create(P)
输出结果如下:
// {}

等一下!!!竟然发现obj4和上边几个方法打印的结果竟然不一样,竟然是个空对象。是因为什么原因呢?

下面就来揭开他的神秘面纱...

首先,在《JavaScript权威指南》这本书上写到,使用Object.create()可以创建一个新对象,其中第一个参数是这个对象的原型。使用它的方法很简单,只需传入所需的原型对象即可,如下:

var o1 = Object.create({x: 1, y: 2}) // o1继承了属性x和y

因此o1是继承了{x: 1, y: 2},打印o1结果为{}

同理,上例中obj4也就是继承了P。 因此obj4打印结果为{},而P则在其对应的原型链上,我们通过原型链向上查找

obj4.__proto__  // 输出 {name: "obj"}

可以看出P在其原型链上。

如果想创建一个普通的空对象(比如通过{}new Object()创建的对象),需要传入Object.prototype:

var o3 = Object.create(Object.prototype) // o3和{}和new Object()一样

第一题到这儿就结束了,是不是有一群小伙伴就倒在了Object.create()这里...

2. 什么是原型?

函数原型,函数有一个属性叫prototype,函数的这个原型指向一个对象,这个对象叫原型对象。这个原型对象有一个constructor属性,指向这个函数本身。关系如下图所示:

3. 什么是构造函数?

凡是通过new来操作函数function,这个function就是个构造函数,如方法二中的Fn。构造函数也是函数,任何函数只要用new进行操作,函数本身都可以当做构造函数。

函数都有prototype属性,声明一个函数的时候,js引擎会给函数自动加上prototype属性。

原型对象如何被区分被哪个构造函数引用:constructor(构造器)

Fn.prototype.constructor === Fn // true

4. 什么是实例?

只要是个对象,就是个实例。如:obj1obj2obj3都是实例。

5. 什么是原型链?

Object.prototype是整个原型链的顶端,从那张经典原型链的图可以看出来。原型链是通过prototype__proto__来完成原型链的向上查找。

原型对象原型链之间的关系和作用:如果构造函数中增加了很多属性和方法,那么它的实例就可以共用这些属性和方法,当有多个实例的时候,想共用这些实例和方法的时候,此时原型对象就上场了,可以将属性和方法存到原型对象(Fn.prototype)上。

通过在原型对象上增加属性和方法以后,通过原型链的方式,实例对象是可以找到原型对象上增加属性和方法,实例也是可以拥有的

任何一个实例对象,通过原型链,找到它上面的原型对象上面的属性和方法,这些属性和方法都是被实例所共享的。如下:

var Fn = function() {
  this.name='obj'
}
var obj3 = new Fn()

Fn.prototype.run = function() {
    console.log('run')
}
var obj5 = new Fn()
obj3.run() // run
obj5.run() // run
// 两个实例上有相同的方法

js引擎的分析方式: 在访问一个实例的时候,先看实例本身上有什么方法,当在这个实例本身没有找到相关方法或属性的时候,就会通过__proto__向原型对向上查找,通过__proto__逐层向上查找,直到Object.prototype,如果依旧未找到,就返回该方法或属性未被定义。

  1. 构造函数(函数)才会有prototype,对象是没有prototype
  2. 只有new出来的实例有__proto__。函数既是函数也是对象,因此也有__proto__
// 这几个关系非常重要,参考神图
Function.__proto__ === Function.prototype // true
Object.prototype.constructor === Object // true
Object.__proto__ === Function.prorotype // true
Function === Function.prorotype.constructor // true

可以理解为:Fn的构造函数式Function。Fn这个普通的函数是Function这个构造函数new出来的一个实例,也就是经典图中的最外层的连线的示例!!!

6. instanceof的原理是什么?

在解释其原理前,我们先知道其作用是什么,参考:instanceof

作用:instanceof运算符用于检测构造函数的prototype属性(constructor.prototype)是否出现在某个实例对象(参数object)的原型链上。

判断实例对象是否为构造函数的实例的时候,判断的是实例对象的__proto__属性和构造函数的prototype属性指向的是否为同一个地址。只要是在个原型链上,instanceof返回的都为true

obj3 instanceof Fn // true
obj3 instanceof Object //true

原理

obj3.__proto__ === M.prototype // true
M.prototype.__proto__ === Object.prototype // true
因此
obj3 instanceof Object //true

因此obj3不一定是Object的一个实例。尤其是A继承了BB继承了C的时候,此时,A生成的实例对象,用instanceof判断A、BC都返回true,此时就无法判断是谁的实例。此时就需要用constructor属性来判断了。

obj3.__proto__.constructor === M // true
obj3.__proto__.constructor === Object // false

因此,在判断实例对象是否为构造函数的实例的时候,用constructor判断比用instanceof更加严谨!

7. new运算符的原理是什么?

new运算符的操作过程如下:

  1. 一个新对象被创建。它继承自Fn.prototype
  2. 构造函数Fn被执行。执行的时候,相应的参数会被传入,同时,上下文(this)会被指定为这个新实例。new Fn等同于new Fn(),但只能用在不传递任何参数的情况下。
  3. 如果构造函数返回了一个“对象”,那么这个对象会取代整个new出来的结果。如果构造函数没有返回对象,那么new出来的结果为步骤1创建的对象.

实现new运算符的效果

var new2 = function(func) {
    // 生成新对象,并指定构造函数的新对象
    // 新创建的对象继承自func构造函数的原型对象
    var o = Object.create(func.prototype)
    // 执行构造函数,并转移上下文
    var k = func.call(o)
    // 判断是否为对象
    if (typeof k === 'object') {
        return k
    } else {
        reutrn o
    }
}

验证

var obj6 = new2(Fn)
obj6 instanceof Fn // true
obj6 instanceof Object // true
obj6.__proto__.constructor === Fn // true
// 在原型链上增加方法
Fn.prototype.walk = function() {
    console.log('walk')
}
obj6.walk() // walk
obj3.walk() // walk

new运算符带参数

function newOperator(ctor, ...args) {
  if (typeof ctor !== 'function) {
    throw new TypeError('Type Error')
  }
  const obj = Object.create(ctor.prototype)
  const res = ctor.apply(obj, args)
  
  const isObject = typeof res === 'object' && res !== null
  const isFunction = typeof res = 'function'
  return isObject || isFunction ? res : obj
}

8. 如何实现继承?

function Parent() {
    this.name = 'parent'
    this.play = [1,2,3]
}
function Child() {
    Parent.call(this)
    this.type = 'child'
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
var s = new Child()
console.log(s instanceof Child, s instanceof Parent)
console.log(s.constructor)

如果再有人问你上面的问题,就让问你的人面对疾风吧...

延伸:

__proto__特性:

  1. 对象特有(function也是对象)
  2. __proto__指向上层(创建自己的那个构造函数)的prototype
  3. 对象可以从prototype中继承属性和方法

prototype特性:

  1. prototype是函数特有的
  2. prototype用于存储共享的属性和方法

constructor特性:

  1. 函数特有,定义在prototype
  2. 通过new创建实例时,该实例便继承了prototype的属性和方法

Object:既是对象,也是构造函数

  • 作为对象:Object.__proto__ === Function.prototype
  • 作为函数:Object.prototype是原型链的顶端,Object.prototype.__ptoto__ = null

Function:既是对象,也是构造函数

  • 作为对象:Function.__protp__ === Function.prototype
  • 作为函数: Function.prototype用于共享,而Function.prototype.__proto__ === Object.prototype

Array(Date 等):既是对象,也是构造函数

  • 作为对象: Array.__proto__ === Function.prototype
  • 作为函数:Array.prototype用于共享,Array.prototype.__proto__ === Object.prototype

对象Parent:既是对象,也是构造函数

  • 作为对象: Parent.__proto__ === Function.prototype
  • 作为函数: Parent.prototype.__proto__ === Object.prototype

附:

  1. 原型链顶端是Object.prototype
  2. 构造函数创建的对(Object、Function、Array、普通对象等)都是Function的实例,他们的__proto__均指向Function.prototype
  3. 除了Object,所有对象(构造函数)的prototype,均继承自Object.prototype
// 需要理清楚是由谁创造出来的
Object.prototype.a = 'a'
Function.prototype.a = 'a1'
function Person() {} // Person是由Function.prototype创造出来的,Person.__proto__ === Function.prototype
console.log(Person.a) // 'a1'
var p = new Person()
console.log(p.a) // 'a'
p.__proto__ === Person.prototype // 在Person.prototype没有a,继续往上查找
p.__proto__.__proto__ === Person.prototype.__proto__
p.__proto__.__proto__ === Object.protoype // 在Object.protoype上有a

p.__proto__.__proto__.constructor.constructor.constructor

p.__proto__.__proto__.constructor // Object() { [native code] }
此时Object由谁创造
Function.prototype创造了ObjectObject上没有constructor就会找Function.prototype的constructor
Object.constructor // Function() { [native code] }
因此 Object.constructor 到了 Function,再往上找constructor时,Function上没有constructor,就会找到Function.prototype上的constructor,就又回到了Function来,因此不管之后又多少个.constructor,永远返回Function!!!

有4个核心点:

  1. Object是由Function.prototype创造的
  2. Function.prototype.__proto__ === Object.prototype
  3. Function自己创造自己,非常重要,即 Function.__proto__ === Function.prototype。函数也是被Function创建的,那么函数的__ptoto__也应该指向Function的原型(Function.prototype),这是一个环形结构,函数Functionprototype__proto__属性指向同一个对象,Fucntion.prototypeObject.protoype是两个特殊的对象,他么由js引擎来创造。
  4. js中万物皆对象
Object.prototype.a = 'a'
function Person() {}
console.log(Person.a) // a

native code 是平台原生代码 Function自己创造自己

Function.prototype === Function.prototype
Function.prototype.constructor === Function

相关题目:

//1. 写出打印结果,并说出原因

var A = function(){}
A.prototype.n = 1;
var b = new A(); // b的__proto__指向n的地址
A.prototype = { // 把A的prototype指针改变
  n: 2,
  m: 3
}
var c = new A();
console.log(b.n, b.m, c.n, c.m) // b.nb.m 、 c.n,c.m 都是通过__proto__向上进行查找的
// 1, undefined,23
// 2. 看原型链

Object.prototype.a = 'a'
Function.prototype.a = 'a1'
function Person() {}
console.log(Person.a) // a1
var xiaohong = new Person()
console.log(xiaohong.a) // a

xiaohong.__proto__ === Person.prototype
Person.__proto__ === Object.prototype
Object.prototype.a = 'a'

Person.__proto__ === Function.prototype

xiaohong.__proto__.__proto__.constructor.constructor.constructor.constructor // Function() { [native code] }  function是由自己创建的自己

xiaohong.__proto__.__proto__ ===  Object.prototype
Object.prototype.a = 'a'
function Person() {}
console.log(Person.a) // a

为什么Object.prototype和Function.prototype差别这么大? Function.proto === Function.prototype // true