彻底理解 JavaScript 原型对象与原型链

原型对象与原型链


JavaScript 的核心之一,也是我们必须掌握的知识点。首先打一枚预防针,这块涉及到的知识点很多,很抽象,比较难理解,但是我尽量阐述清楚,剩下靠你们悟性

我先列一下会出现的几个关键词(如果你全部了解,那就没必要看本文了):

  • 原型对象原型链构造函数

  • [[Prototype]]prototype__proto__this


1、this 的动态绑定(预备知识)

在学习原型之前,我们必须了解 JavaScript 中 this 的动态绑定问题。在 Java 或者 C++ 等编程语言中,this 代表当前实例对象的引用,而且这个引用是和当前对象绑定在一起,不会改变。但是在 JavaScript 中,this 的指向可不是这么一回事,我们先看一个简单的例子:

var date = "Monday";		//这实际上就是 var window.date = "Monday";

function func() {
    /* 你可能会好奇,这只是一个函数,哪来的 this?
    还记得 window 全局对象吗,this 默认绑定的就是它,所以下面这句话实际上输出的是 window.date */
    console.log("Today is " + this.date);
}

func();

//接下来我们写一个对象,往对象中添加 date 属性
var obj = {
    date: "Sunday",
    func: function () {
        //此时的 this 绑定的就是 obj 对象
        console.log("Today is " + this.date);
    }
}
obj.func();

控制台输出结果(这里我顺便打印了一下两个函数的 this 指向):

彻底理解 JavaScript 原型对象与原型链


关于 this 我们就先说这么多,目前只需要知道两点:

  • this 并不指向函数本身
  • this 的绑定发生在函数被调用时,指向的地方完全取决于函数在哪里被调用


2、函数的 prototype

在 JavaScript 中,每一个函数都有一个 prototype 属性,这个属性指向函数的原型对象,然后这个对象有一个 constructor 属性,指向这个函数本身。一句话中连续出现了好几个新概念,不出意外你应该是晕了,没事,我们用图表示一下:

彻底理解 JavaScript 原型对象与原型链


绕了一圈,又回到函数本身,有用吗?那必然是有用的,没用人家会这么干?


我们都知道当一个实例对象被创建的时候会调用它的构造函数。那么既然 JavaScript 中函数的 prototype 指向一个对象,而且这个对象也存在构造函数,是不是就可以利用函数创建实例对象!

如果你能理解上边说的,那你已经成功第一步了,第二步就是如何创建的问题,其实很简单,用 new 呗(没错,在JavaScript 中也存在 new 哦,害怕 new 的同学,不要逃避),let 对象名 = new 函数名(),乍一看函数没有构造函数,但是它的原型对象有啊,当我们 new 函数名() 的时候,函数会通过 prototype 属性找到它的原型对象,原型对象再调用它的构造函数,用代码表示就是 func.prototype.constructor,聪明的你一定发现了,哎,那不就是函数本身嘛,不信你试试 console.log(func === func.prototype.constructor) 看看输出是不是 true。总结一下,我们可以将函数充当为构造函数,利用 new 关键字创建实例对象

光创建实例对象我们还不够,因为这样的对象并不具备任何属性和方法,这时我们想起构造函数往往和 this 手拉手,又因为这里的构造函数就是函数本身,是不是可以直接在函数中使用 this ?答案是肯定的,而且就是利用 JavaScript 中 this 的动态绑定问题。上文说过 this 默认绑定 window 对象,但是在 new 的时候,由于是原型对象在调用构造函数(也就是函数本身),这时候 this 就会和待创建的实例对象绑定在一起,自然设置的就是对象的属性,如果你不能很好的理解这句话,记得仔细看代码

属性的问题我们已经解决,最后还差方法,由于我们是想让函数的原型对象具有方法,所以直接在函数上设置没用。那该怎么办?其实很简单哦,我们只需要使用 prototype 属性找到函数的原型对象,直接利用 对象.方法名 = funtion(参数) {} 的方式即可(忘了的同学看看 JavaScript 对象相关的基本知识)

//对了,这里说一个规范,当这个函数我们准备充当构造函数的时候,需要大写首字母哦
function Person(obj) {
    // || 右边的代表默认值,即不赋值的情况下属性的默认值
    this.name = obj.name || "UnName";
    this.age = obj.age || "-1";
}

//直接在函数上面添加方法(错误)
//Person.run = function(){
//	console.log(this.name + " 在跑步");
//}

//使用 prototype 增加 Person 原型对象的方法(正确)
Person.prototype.run = function () {
    console.log(this.name + " 在跑步");
}

Person.prototype.eat = function () {
    console.log(this.name + " 在吃饭");
}

let Mike = new Person({name: "Mike", age: 21});
Mike.eat();					//输出 Mike 在吃饭
Mike.run();					//输出 Mike 在跑步

好,那我们可以把上边的图更新一下:

彻底理解 JavaScript 原型对象与原型链



3、对象的 [[Prototype]]( __proto__ )

终于讲到对象了,JavaScript 的每一个对象都有一个 被隐藏 的属性,称为 [[Prototype]](一定不要去掉两旁的中括号!在 Chrome 等大多数浏览器中这个也被命名为 __proto__),[[Prototype]] 我们可以理解成一个指针,指向一个对象,就是当前对象的 原型对象,没错,就是函数的那个原型对象。当 JavaScript 在当前对象找不到一个属性或者方法的时候,就会通过 [[Prototype]] 委托给它的原型对象(委托:简单点理解就是在原型对象中找我们想要的属性和方法)

那我们继续更新图:

彻底理解 JavaScript 原型对象与原型链


从上图我们可以看到函数的 prototype 和 对象的 [[Prototype]] 指向同一个原型对象,我们验证一下是不是:

//还是用上边 Person 代码哈,浏览器中 __proto__ 就是 [[Prototype]]
console.log(Mike.__proto__ === Person.prototype);		//输出 true,自己动手,丰衣足食


4、原型链

原型链是什么,当我们要在一个对象(假设对象 A)*问一个属性,JavaScript 首先会看对象 A 本身存不存在目标属性,若不存在,则会通过它的 [[Prototype]] 查看它的原型对象(假设对象 B)存不存在待查找属性,若还是不存在,则继续查找对象 B 的原型对象(假设对象 C)存不存在目标属性,一直到找到属性或者返回 null(就是找到 Object 了,还是没找到属性),其中 对象 A —> 对象 B —> 对象 C —> … —> Object 就是原型链的基本模型

最后更新一次关系图:

彻底理解 JavaScript 原型对象与原型链



那么关于prototype、 [[Prototype]]、原型对象、原型链的所有理论知识介绍完毕,我们做一个总结:

  • 每一个函数都有 prototype 属性,指向它的原型对象
  • 函数的原型对象有一个 constructor 属性,指向它的构造函数,就是函数本身
  • 每一个对象有一个 [[Prototype]] 属性,指向它的原型对象
  • 原型对象还可以通过 [[Prototype]] 指向它的原型对象,即原型的原型
  • 通过一个个原型对象形成了原型链
上一篇:js 继承由浅入深


下一篇:JS原型