javascript原型与原型链

说到JavaScript的原型和原型链,相关文章已有很多,可是大都晦涩难懂。本文将换一个角度出发,先理解原型和原型链是什么,有什么做用,再去分析那些使人头疼的关系。数组

1、引用类型皆为对象

原型和原型链都是来源于对象而服务于对象的概念,因此咱们要先明确一点:bash

JavaScript中一切引用类型都是对象,对象就是属性的集合。app

Array类型Function类型Object类型Date类型RegExp类型等都是引用类型。函数

也就是说 数组是对象、函数是对象、正则是对象、对象仍是对象。测试

2、原型和原型链是什么

上面咱们说到对象就是属性(property)的集合,有人可能要问不是还有方法吗?其实方法也是一种属性,由于它也是键值对的表现形式,具体见下图。ui

能够看到obj上确实多了一个sayHello的属性,值为一个函数,可是问题来了,obj上面并无hasOwnProperty这个方法,为何咱们能够调用呢?这就引出了 原型this

每个对象从被建立开始就和另外一个对象关联,从另外一个对象上继承其属性,这个另外一个对象就是 原型spa

当访问一个对象的属性时,先在对象的自己找,找不到就去对象的原型上找,若是仍是找不到,就去对象的原型(原型也是对象,也有它本身的原型)的原型上找,如此继续,直到找到为止,或者查找到最顶层的原型对象中也没有找到,就结束查找,返回undefinedprototype

这条由对象及其原型组成的链就叫作原型链。code

如今咱们已经初步理解了原型和原型链,到如今你们明白为何数组均可以使用pushslice等方法,函数可使用callbind等方法了吧,由于在它们的原型链上找到了对应的方法。

OK,总结一下

  1. 原型存在的意义就是组成原型链:引用类型皆对象,每一个对象都有原型,原型也是对象,也有它本身的原型,一层一层,组成原型链。
  2. 原型链存在的意义就是继承:访问对象属性时,在对象自己找不到,就在原型链上一层一层找。说白了就是一个对象能够访问其余对象的属性。
  3. 继承存在的意义就是属性共享:好处有二:一是代码重用,字面意思;二是可扩展,不一样对象可能继承相同的属性,也能够定义只属于本身的属性。

3、建立对象

对象的建立方式主要有两种,一种是new操做符后跟函数调用,另外一种是字面量表示法。

目前咱们如今能够理解为:全部对象都是由new操做符后跟函数调用来建立的,字面量表示法只是语法糖(即本质也是new,功能不变,使用更简洁)。

// new操做符后跟函数调用
let obj = new Object()
let arr = new Array()

// 字面量表示法
let obj = { a: 1}
// 等同于
let obj = new Object()
obj.a = 1

let arr = [1,2]
// 等同于
let arr = new Array()
arr[0] = 1
arr[1] = 2
复制代码复制代码

ObjectArray等称为构造函数,不要怕这个概念,构造函数和普通函数并无什么不一样,只是因为这些函数常被用来跟在new后面建立对象。new后面调用一个空函数也会返回一个对象,任何一个函数均可以当作构造函数

因此构造函数更合理的理解应该是函数的构造调用

NumberStringBooleanArrayObjectFunctionDateRegExpError这些都是函数,并且是原生构造函数,在运行时会自动出如今执行环境中。

构造函数是为了建立特定类型的对象,这些经过同一构造函数建立的对象有相同原型,共享某些方法。举个例子,全部的数组均可以调用push方法,由于它们有相同原型。

咱们来本身实现一个构造函数:

// 惯例,构造函数应以大写字母开头
function Person(name) {
  // 函数内this指向构造的对象
  // 构造一个name属性
  this.name = name
  // 构造一个sayName方法
  this.sayName = function() {
    console.log(this.name)
  }
}

// 使用自定义构造函数Person建立对象
let person = new Person('logan')
person.sayName() // 输出:logan
复制代码复制代码

总结一下构造函数用来建立对象,同一构造函数建立的对象,其原型相同。

4、__proto__与prototype

万物逃不开真香定律,初步了解了相关知识,咱们也要试着来理解一下这些头疼的单词,而且看一下指来指去的箭头了。

上面总结过,每一个对象都有原型,那么咱们怎么获取到一个对象的原型呢?那就是对象的__proto__属性,指向对象的原型。

上面也总结过,引用类型皆对象,因此引用类型都有__proto__属性,对象有__proto__属性,函数有__proto__属性,数组也有__proto__属性,只要是引用类型,就有__proto__属性,都指向它们各自的原型对象。

__proto__属性虽然在ECMAScript 6语言规范中标准化,可是不推荐被使用,如今更推荐使用Object.getPrototypeOfObject.getPrototypeOf(obj)也能够获取到obj对象的原型。本文中使用__proto__只是为了便于理解。

Object.getPrototypeOf(person) === person.__proto__ // true
复制代码复制代码

上面说过,构造函数是为了建立特定类型的对象,那若是我想让Person这个构造函数建立的对象都共享一个方法,总不能像下面这样吧:

错误示范

// 调用构造函数Person建立一个新对象personA
let personA = new Person('张三')
// 在personA的原型上添加一个方法,以供以后Person建立的对象所共享
personA.__proto__.eat = function() {
    console.log('吃东西')
}
let personB = new Person('李四')
personB.eat() // 输出:吃东西
复制代码复制代码

可是每次要修改一类对象的原型对象,都去建立一个新的对象实例,而后访问其原型对象并添加or修改属性总以为画蛇添足。既然构造函数建立的对象实例的原型对象都是同一个,那么构造函数和其构造出的对象实例的原型对象之间有联系就完美了。

这个联系就是prototype。每一个函数拥有prototype属性,指向使用new操做符和该函数建立的对象实例的原型对象。

Person.prototype === person.__proto__ // true
复制代码复制代码

看到这里咱们就明白了,若是想让Person建立出的对象实例共享属性,应该这样写:

正确示范

Person.prototype.drink = function() {
    console.log('喝东西')
}

let personA = new Person('张三')
personB.drink() // 输出:喝东西
复制代码复制代码

OK,惯例,总结一下

  1. 对象有__proto__属性,函数有__proto__属性,数组也有__proto__属性,只要是引用类型,就有__proto__属性,指向其原型。
  2. 只有函数有prototype属性,只有函数有prototype属性,只有函数有prototype属性,指向new操做符加调用该函数建立的对象实例的原型对象。

5、原型链顶层

原型链之因此叫原型链,而不叫原型环,说明它是善始善终的,那么原型链的顶层是什么呢?

拿咱们的person对象来看,它的原型对象,很简单

// 1. person的原型对象
person.__proto__ === Person.prototype
复制代码复制代码

接着往上找,Person.prototype也是一个普通对象,能够理解为Object构造函数建立的,因此得出下面结论,

// 2. Person.prototype的原型对象
Person.prototype.__proto__ === Object.prototype
复制代码复制代码

Object.prototype也是一个对象,那么它的原型呢?这里比较特殊,切记!!!

Object.prototype.__proto__ === null
复制代码复制代码

咱们就能够换个方式描述下 原型链 :由对象的__proto__属性串连起来的直到Object.prototype.__proto__(为null)的链就是原型链。

在上面内容的基础之上,咱们来模拟一下js引擎读取对象属性:

function getProperty(obj, propName) {
    // 在对象自己查找
    if (obj.hasOwnProperty(propName)) {
        return obj[propName]
    } else if (obj.__proto__ !== null) {
    // 若是对象有原型,则在原型上递归查找
        return getProperty(obj.__proto__, propName)
    } else {
    // 直到找到Object.prototype,Object.prototype.__proto__为null,返回undefined
        return undefined
    }
}
复制代码复制代码

6、constructor

回忆一下以前的描述,构造函数都有一个prototype属性,指向使用这个构造函数建立的对象实例的原型对象

这个原型对象中默认有一个constructor属性,指回该构造函数。

Person.prototype.constructor === Person // true
复制代码复制代码

之因此开头不说,是由于这个属性对咱们理解原型及原型链并没有太大帮助,反而容易混淆。

7、函数对象的原型链

以前提到过引用类型皆对象,函数也是对象,那么函数对象的原型链是怎么样的呢?

对象都是被构造函数建立的,函数对象的构造函数就是Function,注意这里F是大写。

let fn = function() {}
// 函数(包括原生构造函数)的原型对象为Function.prototype
fn.__proto__ === Function.prototype // true
Array.__proto__ === Function.prototype // true
Object.__proto__ === Function.prototype // true
复制代码复制代码

Function.prototype也是一个普通对象,因此Function.prototype.__proto__ === Object.prototype

这里有一个特例,Function__proto__属性指向Function.prototype

总结一下:函数都是由Function原生构造函数建立的,因此函数的__proto__属性指向Functionprototype属性

8、小试牛刀

真香警告!

有点乱?没事,咱们先将以前的知识都总结一下,而后慢慢分析此图:

知识点

  1. 引用类型都是对象,每一个对象都有原型对象。
  2. 对象都是由构造函数建立,对象的__proto__属性指向其原型对象,构造函数的prototype属性指向其建立的对象实例的原型对象,因此对象的__proto__属性等于建立它的构造函数的prototype属性。
  3. 全部经过字面量表示法建立的普通对象的构造函数为Object
  4. 全部原型对象都是普通对象,构造函数为Object
  5. 全部函数的构造函数是Function
  6. Object.prototype没有原型对象

OK,咱们根据以上六点总结来分析上图,先从左上角的f1f2入手:

// f一、f2都是经过new Foo()建立的对象,构造函数为Foo,因此有
f1.__proto__ === Foo.prototype
// Foo.prototype为普通对象,构造函数为Object,因此有
Foo.prototype.__proto === Object.prototype
// Object.prototype没有原型对象
Object.prototype.__proto__ === null
复制代码复制代码

而后对构造函数Foo下手:

// Foo是个函数对象,构造函数为Function
Foo.__proto__ === Function.prototype
// Function.prototype为普通对象,构造函数为Object,因此有
Function.prototype.__proto__ === Object.prototype
复制代码复制代码

接着对原生构造函数Object建立的o1o2下手:

// o一、o2构造函数为Object
o1.__proto__ === Object.prototype
复制代码复制代码

最后对原生构造函数ObjectFunction下手:

// 原生构造函数也是函数对象,其构造函数为Function
Object.__proto__ === Function.prototype
// 特例
Function.__proto__ === Function.prototype
复制代码复制代码

分析完毕,也没有想象中那么复杂是吧。

若是有内容引发不适,建议从头看一遍,或者去看看参考文章内的文章。

9、触类旁通

1. instanceof操做符

日常咱们判断一个变量的类型会使用typeof运算符,可是引用类型并不适用,除了函数对象会返回function外,其余都返回object。咱们想要知道一个对象的具体类型,就须要使用到instanceof

let fn = function() {}
let arr = []
fn instanceof Function // true
arr instanceof Array // true
fn instanceof Object // true
arr instanceof Object // true
复制代码复制代码

为何fn instanceof Objectarr instanceof Object都返回true呢?咱们来看一下MDN上对于instanceof运算符的描述:

instanceof运算符用于测试构造函数的prototype属性是否出如今对象的原型链中的任何位置

也就是说instanceof操做符左边是一个对象,右边是一个构造函数,在左边对象的原型链上查找,知道找到右边构造函数的prototype属性就返回true,或者查找到顶层null(也就是Object.prototype.__proto__),就返回false。 咱们模拟实现一下:

function instanceOf(obj, Constructor) { // obj 表示左边的对象,Constructor表示右边的构造函数
    let rightP = Constructor.prototype // 取构造函数显示原型
    let leftP = obj.__proto__ // 取对象隐式原型
    // 到达原型链顶层还未找到则返回false
    if (leftP === null) {
        return false
    }
    // 对象实例的隐式原型等于构造函数显示原型则返回true
    if (leftP === rightP) {
        return true
    }
    // 查找原型链上一层
    return instanceOf(obj.__proto__, Constructor)
}
复制代码复制代码

如今就能够解释一些比较使人费解的结果了:

fn instanceof Object //true
// 1. fn.__proto__ === Function.prototype
// 2. fn.__proto__.__proto__ === Function.prototype.__proto__ === Object.prototype
arr instanceof Object //true
// 1. arr.__proto__ === Array.prototype
// 2. arr.__proto__.__proto__ === Array.prototype.__proto__ === Object.prototype
Object instanceof Object // true
// 1. Object.__proto__ === Function.prototype
// 2. Object.__proto__.__proto__ === Function.prototype.__proto__ === Object.prototype
Function instanceof Function // true
// Function.__proto__ === Function.prototype
复制代码复制代码

总结一下:instanceof运算符用于检查右边构造函数的prototype属性是否出如今左边对象的原型链中的任何位置。其实它表示的是一种原型链继承的关系。

2. Object.create

以前说对象的建立方式主要有两种,一种是new操做符后跟函数调用,另外一种是字面量表示法。

其实还有第三种就是ES5提供的Object.create()方法,会建立一个新对象,第一个参数接收一个对象,将会做为新建立对象的原型对象,第二个可选参数是属性描述符(不经常使用,默认是undefined)。具体请查看Object.create()

咱们来模拟一个简易版的Object.create

function createObj(proto) {
    function F() {}
    F.prototype = proto
    return new F()
}
复制代码复制代码

咱们日常所说的空对象,其实并非严格意义上的空对象,它的原型对象指向Object.prototype,还能够继承hasOwnPropertytoStringvalueOf等方法。

若是想要生成一个不继承任何属性的对象,可使用Object.create(null)

若是想要生成一个日常字面量方法生成的对象,须要将其原型对象指向Object.prototype

let obj = Object.create(Object.prototype)
// 等价于
let obj = {}
复制代码复制代码

3. new操做符

当咱们使用new时,作了些什么?

  1. 建立一个全新对象,并将其__proto__属性指向构造函数的prototype属性。
  2. 将构造函数调用的this指向这个新对象,并执行构造函数。
  3. 若是构造函数返回对象类型Object(包含Functoin, Array, Date, RegExg, Error等),则正常返回,不然返回这个新的对象。

依然来模拟实现一下:

function newOperator(func, ...args) {
    if (typeof func !== 'function') {
        console.error('第一个参数必须为函数,您传入的参数为', func)
        return
    }
    // 建立一个全新对象,并将其`__proto__`属性指向构造函数的`prototype`属性
    let newObj = Object.create(func.prototype)
    // 将构造函数调用的this指向这个新对象,并执行构造函数
    let result = func.apply(newObj, args)
    // 若是构造函数返回对象类型Object,则正常返回,不然返回这个新的对象
    return (result instanceof Object) ? result : newObj
}
复制代码复制代码

4. Function.__proto__ === Function.prototype

其实这里彻底不必去纠结鸡生蛋仍是蛋生鸡的问题,我本身的理解是:Function是原生构造函数,自动出如今运行环境中,因此不存在本身生成本身。之因此Function.__proto__ === Function.prototype,是为了代表Function做为一个原生构造函数,自己也是一个函数对象,仅此而已。

5. 真的是继承吗?

前面咱们讲到每个对象都会从原型“继承”属性,实际上,继承是一个十分具备迷惑性的说法,引用《你不知道的JavaScript》中的话,就是:

继承意味着复制操做,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间建立一个关联,这样,一个对象就能够经过委托访问另外一个对象的属性,因此与其叫继承,委托的说法反而更准确些。

相关文章
相关标签/搜索