一文让你对js的原型与原型链不再害怕、迷惑

原型与原型链的详细剖析

写在最前: 希望各位看完这篇文章后,再也不用害怕JS原型链部分的知识! — by Fitz

一起努力,加油吧!

原型

原型分为两种显式原型prototype隐式原型__proto__

显式原型prototype

显式原型prototype存在于函数中,是函数的一个属性,它默认指向一个Object空对象(原型对象)
注意: Object空对象(原型对象)只是内容为空, 并不是真正意义上的空对象Object.create(null), 还是能够通过原型链看到Object.prototype上的各种属性、方法,例如: toString()

console.log(Object.prototype)
console.log(typeof Object.prototype)    // object

函数原型对象上的constructor属性对应的函数对象自身

console.log(Object.prototype.constructor === Object)    // true
/* 
    例如:
        我定义了一个叫Test的函数
        Test这个函数拥有它自己的prototype原型对象
        原型对象上的constructor属性对应的就是Test函数自己
*/
console.log(test.prototype.constructor === test)    // true

实例都会自动拥有其函数(构造函数)原型对象上的方法、属性

function Person () {}
Person.prototype.sayHello = function () {
    console.log('Hello')
}

var fitz = new Person() // fitz是Person构造函数的一个实例
fitz.sayHello() // 'Hello'

隐式原型__proto__

每个实例对象都拥有隐式原型属性__proto__

function Student () {
    // 构造函数Student
}
let fitz = new Student()    // fitz是Student的实例对象
console.log(fitz.__proto__) // {constructor: ƒ}

显式原型prototype与隐式原型__proto__的关系

  1. 构造函数的显式原型prototype默认指向一个空的(没有我们自己定义的属性、方法)Object对象
  2. 构造函数的每个实例上都有的隐式原型__proto__, 都指向着构造函数的显式原型prototype

实例对象的隐式原型属性就是其构造函数的显式原型属性

function Student () {
    // 构造函数Student
}
let fitz = new Student()    // fitz是Student的实例对象
console.log(fitz.__proto__ === Student.prototype) // true

关于原型对象创建整体的流程

function Person () {}   // 函数创建的时候,JS引擎为Person函数自动添加prototype对象属性, 属性指向一个空的Object对象

let fitz = new Person() // 实例对象创建的时候, JS引擎自动添加__proto__对象属性, 同时将这个__proto__指向该实例对象的构造函数的prototype

/* 
    JS引擎自动做了的事:
        1.   Person.prototype = new Object()
        2.   per1.__proto__ = Person.prototype
*/

原型链(隐式原型链)

原型链指的是: 在访问一个对象中的属性时,会先在自身中寻找,如果没有找到就会沿着__proto__向上寻找,如果找到就返回属性,没有就返回undefined

function Student () {
    this.sayName = function () {
        console.log('Fitz')
    }
}
// 向Student的显示原型对象上添加sayAge()方法
Student.prototype.sayAge = function () {
    console.log(21)
}

var a = new Student()
a.sayName()     // 'Fitz'
a.sayAge()      // 21
console.log(a.toString())   // [Object object]

探寻原型链的尽头

首先,理清从自定义实例对象Object构造函数的prototype的关系

// Object构造函数是JS引擎定义、生成的
console.log(Object)
// 查看Object的显示原型对象
console.log(Object.prototype) // 能够看到toString()等方法

// 自定义一个Student构造函数
function Student () {}
const stu = new Student()   // 创建一个Student实例对象

/* 
    因为原型链就是隐式原型链,本质上是沿着隐式原型属性__proto__
    向上寻找属性、方法的一个过程
*/

// 所以我们通过stu实例对象探寻原型链的尽头
console.log(stu.__proto__) // 实例stu的隐式原型
// 实例对象的__proto__ 指向  它构造函数的prototype
console.log(stu.__proto__ === Student.prototype) // true
// 构造函数的prototype默认是一个空的Object实例对象
console.log(Student.prototype)
// 空的Object实例对象的构造函数一定是Object构造函数
console.log(Student.prototype.__proto__ === Object.prototype) //true
/* 
    到这里,暂时总结一下此时的原型链状态:
    stu.__proto__ => Student.prototype => object.__proto__ => Object.prototype
*/

然后就是最关键的部分: 着重理清Object构造函数的prototype往后部分的所有内容

// Object构造函数是JS引擎定义、生成的
console.log(Object)
// 查看Object的显示原型对象
console.log(Object.prototype) // 能够看到toString()等方法

// 最为关键的一步, 这一步直接揭示了原型链的尽头在哪
console.log(Object.prototype.__proto__) // null

由此我们就能知道,原型链的尽头就是: Object.prototype

// Object构造函数是JS引擎定义、生成的
console.log(Object)
// 查看Object的显示原型对象
console.log(Object.prototype) // 能够看到toString()等方法

// 自定义一个Student构造函数
function Student() { }
const stu = new Student()   // 创建一个Student实例对象

/* 
    因为原型链就是隐式原型链,本质上是沿着隐式原型属性__proto__
    向上寻找属性、方法的一个过程
*/

// 所以我们通过stu实例对象探寻原型链的尽头
console.log(stu.__proto__) // 实例stu的隐式原型
// 实例对象的__proto__ 指向  它构造函数的prototype
console.log(stu.__proto__ === Student.prototype) // true
// 构造函数的prototype默认是一个空的Object实例对象
console.log(Student.prototype)
// 空的Object实例对象的构造函数一定是Object构造函数
console.log(Student.prototype.__proto__ === Object.prototype) //true
/* 
    到这里,暂时总结一下此时的原型链状态:
    stu.__proto__ => Student.prototype => object.__proto__ => Object.prototype
    */

// 最为关键的一步, 这一步直接揭示了原型链的尽头在哪
console.log(Object.prototype.__proto__) // null

/* 
    到这里,我们就能总结出原型链的尽头就是Object.prototype的结论:
    stu.__proto__ => Student.prototype => object.__proto__ => Object.prototype => null
*/

完整详尽的分析原型链

基于这一张图,我们就能够比较全面的掌握JavaScript中原型链的概念,在分析前,小伙伴们可以看这张图先自己思考一遍

接下来是全面总结、分析原型链知识的部分

/* 
    根据上面这张图由易入难,完整分析原型链
*/

// ===============第一部分: 自定义的构造函数及其实例============
function Foo () {}  // 1. 构造函数Foo
var f1 = new Foo()  // 2. 实例对象f1
// 3. 实例对象的隐式原型 指向 其构造函数的显示原型
console.log(f1.__proto__ === Foo.prototype) // true
// 4. 构造函数的显式原型是一个空的object对象
// 5. 这个空的object对象是Object构造函数的实例
console.log(Foo.prototype.__proto__ === Object.prototype) // true
// 6. 自定义构造函数是 Function构造函数的实例
//    换句话说: Foo这个构造函数,是new Function()出来的
console.log(Foo.__proto__ === Function.prototype) // true
// ===============第一部分: 自定义的构造函数及其实例============

// =============第二部分: Object构造函数及原型链的尽头============
console.log(Object) // 1.  ƒ Object() { [native code] }
// 2. 实例对象o1、o2
var o1 = new Object()
var o2 = {}
// 3. Object构造函数也是Function构造函数的实例
//    换句话说: Object这个构造函数,也是new Function()出来的
console.log(Object.__proto__ === Function.prototype) // ture
// 4. Object构造函数的显式原型(Object.prototype)就是原型链的尽头
console.log(Object.prototype.__proto__) // 5. null
// =============第二部分: Object构造函数及原型链的尽头============


// =================第三部分: 特殊Function构造函数================
console.log(Function) // 1.  ƒ Function() { [native code] }
// 2. Function构造函数的原型对象跟其他普通的构造函数一样   隐式原型指向空object对象
console.log(Function.prototype.__proto__ === Object.prototype) // true
// 3. 重点特殊的地方: Function构造函数是自己的实例
//    换句话说: Function构造函数,是new Function()自己出来的, 即我生出我自己
console.log(Function.__proto__ === Function.prototype) // true
// 4. Function.prototype是一个函数,而不是像其他函数一样是一个空的Object对象
console.log(typeof Function.prototype) // function
// =================第三部分: 特殊Function构造函数================

这张图配合上面的代码

关于原型链的补充总结

所有函数都是 Function构造函数 的实例对象,包括Function构造函数自己

标题换句话表达, 所有函数的__proto__都指向Function.prototype
Function.__proto__指向Function.prototype

// 所有函数都是 Function构造函数 的实例对象
/* 换句话说: 无论是 
                普通函数、方法
                自定义构造函数
                Object等一些JS引擎内置的构造函数
                Function构造函数本身(我生我自己)
            都是Function构造函数的实例对象
*/
const sayHello = function () {console.log('hello')} // 自定义函数
const Student = function (name) {    // 自定义构造函数
    this.name = name
}
console.log(sayHello.__proto__=== Function.prototype)
console.log(Student.__proto__=== Function.prototype)
console.log(Object.__proto__=== Function.prototype)
console.log(Date.__proto__=== Function.prototype)
// 最为特殊的Function(我生我自己)
console.log(Function.__proto__=== Function.prototype)

Function.prototype是一个函数对象

console.log(typeof Function.prototype) // function
console.dir(Function.prototype)

所有函数的显式原型prototype,都指向Object.prototype,Object构造函数的显式原型除外

console.log(Function.prototype instanceof Object) // true
console.log(Function.prototype.__proto__ === Object.prototype) // true
console.log(Date.prototype instanceof Object) // true
console.log(Date.prototype.__proto__ === Object.prototype) // true
// object构造函数的原型对象除外的理由, Object.prototype是原型链的尽头
console.log(Object.prototype instanceof Object) // false
console.log(Object.prototype.__proto__ === Object.prototype) // false
console.log(Object.prototype.__proto__) // null

原型链的应用

读取实例对象的属性值时,会先在自身中寻找,如果自身没有会到原型链中找

function Student () {}
Student.prototype.person = 'Fitz'
var f = new Student()
// person属性是原型对象上的,而不是实例本身的
console.log(f.person)   // 'Fitz'

对实例的属性进行操作时,不会影响(查找)原型链,如果实例中没有当前属性,会自动添加

function Student () {}
Student.prototype.person = 'Fitz'
var f = new Student()
// 如果实例中没有当前属性,会自动添加
f.person = 'Lx'
f.age = 21
/* 
    属性只会在实例上,与原型链无关
    可以运用前面的 引用数据类型的知识理解
*/

利用原型链,将实例的方法添加在原型对象上,实例的属性添加在实例自身

好处: 避免了每次实例化对象时,都创建出一模一样的方法,节省内存

function Person (name, age){
    this.name = name
    this.age = age
}
// 实例的方法统一放在构造函数的原型对象上
// 这样实例在调用方法时,可以通过原型链顺利找到该方法
Person.prototype.printInfo = function () {
    console.log(`name: ${this.name}`)
    console.log(`age: ${this.age}`)
}

var fitz = new Person('fitz', 21)
fitz.printInfo()

原型链继承

尝试使用原型链来模拟类的继承

实现的关键是: 子类的原型是父类的实例

思路来源于: 既然所有的实例对象都能调用toString()方法那就看看为什么,

  1. toString()方法在Object.prototype显式原型对象上
  2. 实例对象的隐式原型__proto__ 指向其 构造函数的显式原型prototype
  3. 而关键就是,构造函数的显式原型是Object.prototype的实例对象
// 模拟父类
function Father() {
    _Fathername = '我是父类'
    this.name = 'Father'
}
Father.prototype.getFathername = function () {
    console.log(_Fathername)
}
Father.prototype.getName = function () {
    console.log(this.name)
}

// 模拟子类
function Son() {
    _SonName = '我是子类'
    this.name = 'Son'
}


// 实现子类继承父类
Son.prototype = new Father()


Son.prototype.getSonName = function () {
    console.log(_SonName)
}

// 需要实现的目标
var son = new Son()
// 能在子类使用父类的方法
son.getFathername() // '我是父类'
son.getName() // 'Son'

console.log(son)