关于javascript:彻底搞懂JS原型与原型链

40次阅读

共计 8023 个字符,预计需要花费 21 分钟才能阅读完成。

说到 JavaScript 的原型和原型链,相干文章已有不少,然而大都艰涩难懂。本文将换一个角度登程,先了解原型和原型链是什么,有什么作用,再去剖析那些令人头疼的关系。

一、援用类型皆为对象

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

JavaScript 中所有援用类型都是对象,对象就是属性的汇合。

Array 类型 Function 类型Object 类型Date 类型RegExp 类型 等都是援用类型。

也就是说 数组是对象、函数是对象、正则是对象、对象还是对象。

二、原型和原型链是什么

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

能够看到 obj 上的确多了一个 sayHello 的属性,值为一个函数,然而问题来了,obj下面并没有 hasOwnProperty 这个办法,为什么咱们能够调用呢?这就引出了 原型

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

当拜访一个对象的属性时,先在对象的自身找,找不到就去对象的原型上找,如果还是找不到,就去对象的原型(原型也是对象,也有它本人的原型)的原型上找,如此持续,直到找到为止,或者查找到最顶层的原型对象中也没有找到,就完结查找,返回undefined

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

当初咱们曾经初步了解了原型和原型链,到当初大家明确为什么数组都能够应用 pushslice 等办法,函数能够应用 callbind 等办法了吧,因为在它们的原型链上找到了对应的办法。

OK,总结一下

  1. 原型存在的意义就是组成原型链:援用类型皆对象,每个对象都有原型,原型也是对象,也有它本人的原型,一层一层,组成原型链。
  2. 原型链存在的意义就是继承:拜访对象属性时,在对象自身找不到,就在原型链上一层一层找。说白了就是一个对象能够拜访其余对象的属性。
  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

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

四、__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 操作符加调用该函数创立的对象实例的原型对象。
  3. 参考视频解说:进入学习

五、原型链顶层

原型链之所以叫原型链,而不叫原型环,阐明它是善始善终的,那么原型链的顶层是什么呢?

拿咱们的 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
    }
}

六、constructor

回顾一下之前的形容,构造函数都有一个 prototype 属性,指向应用这个构造函数创立的对象实例的 原型对象

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

Person.prototype.constructor === Person // true

之所以结尾不说,是因为这个属性对咱们了解原型及原型链并无太大帮忙,反而容易混同。

七、函数对象的原型链

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

对象都是被构造函数创立的,函数对象的构造函数就是 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属性

八、小试牛刀

真香正告!

有点乱?没事,咱们先将之前的常识都总结一下,而后缓缓剖析此图:

知识点

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

OK,咱们依据以上六点总结来剖析上图,先从左上角的 f1f2 动手:

// f1、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 下手:

// o1、o2 构造函数为 Object
o1.__proto__ === Object.prototype

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

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

剖析结束,也没有设想中那么简单是吧。

如果有内容引起不适,倡议从头看一遍,或者去看看参考文章内的文章。

九、触类旁通

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 只是在两个对象之间创立一个关联,这样,一个对象就能够通过委托拜访另一个对象的属性,所以与其叫继承,委托的说法反而更精确些。

正文完
 0