前言
对于每一位前端开发人员来说 JavaScript
语言本身是相当重要的,掌握好这一门语言就是我们的立足之本。但是对于语言本身的一些难点,可能我们每次都掌握的不扎实,不能把他挖深了。这一节就针对原型与原型链这一部分内容进行深入的彻底学习,一劳永逸。
本文我会尽量使用通俗易懂的方式阐述,并且加杂着图片辅助大家理解。我会从原型本身的定义讲起直到前端社区中对原型与原型链的经典使用,由潜入深的学习这一块知识。将知识体系化,实践化。相信通过这样,大家都可以一次性的学会原型与原型链。
为什么需要原型
在传统的面向对象语言比如 java
中,使用 class
关键字定义一个类,我们可以创建一个类的实例对象。
但是在我们熟悉的 javascript
这门语言中其实并没有类这一概念,它是使用了一种不常用的方式来实现面向对象这一特性,这种方式就是原型。
所以原型是 javascript
的作者在设计这门语言的时候为了支持面向对象的选择。
prototype(原型对象)是什么
javascript
规定,每一个函数都有一个 prototype
属性,指向另一个对象(原型对象)。
这个原型对象上的所有属性和方法都会被它的构造器函数的实例继承。
我们来创建一个函数,并且打印一下它的 prototype
属性一探究竟:
function Person() {
}
console.log(Person.prototype);
浏览器中执行这段代码,返回结果为:
首先这确实是一个对象,它有两个属性,我们先看 constructor
这一属性。它的值是一个函数,看起来就是 Person
本身,来验证一下:
console.log(Person.prototype.constructor === Person); // true
果然是它本身。所以说 Person
这一函数其实就是它的原型对象的构造器函数。
我们再来看这个构造函数的实例对象会不会继承原型对象上的方法。
首先我会给 Person
的原型对象添加一个 name
属性和 getName
方法,然后访问它的实例对象上有没有继承对应的属性和方法:
function Person() {
};
Person.prototype.name = 'Person 原型对象上的 name 属性';
Person.prototype.getName = function() {
console.log(this.name);
};
const person = new Person();
console.log(person.name);
person.getName();
同样在浏览器中看执行结果:
可以看到得到了预期的结果,这样也就解释了什么是原型对象并且验证了继承这一特性。
__proto__
上面我们已经知道了每一个函数都有一个原型对象,并且构造函数的实例对象会继承这个原型对象的属性和方法。
那么这个实例对象就肯定跟构造函数的原型对象之间存在着某种联系。这一个联系表现在实例对象的 __proto__
这一属性上。
也就是说实例对象的 __proto__
这一属性的属性值指向它的构造函数的原型对象。我们再来加以验证:
function Person() {
};
const person = new Person();
console.log(person.__proto__ === Person.prototype);
可以看到浏览器的执行结果为 true
:
在讲解原型链之前,我们先来做一个铺垫:
其实,对于每一个对象来说,它都有 __proto__
这一属性,注意这里的对象是泛指的。也就是说,对于数组、正则、日期、函数等对象都具有这一属性。因为函数其实也是一个对象,在 javascript
中,万物皆是对象。
我们来验证一下函数具有这一属性:
function Person() {
};
console.log(Person.__proto__);
可以在浏览器中看到:
我们先不管这是什么,起码我们已经验证了函数、普通对象等都具有 __proto__
这一属性,这对后面介绍原型链具有很大帮助。
原型链
了解原型之后,也就到了一个最重要的环节——原型链。
javascript
语言在原型的基础上通过链表这一数据结构将原型成链,成环。搞清楚这一特性后,可以对语言有更深层次的认知,使用层面上也会让你变得游刃有余。
先来将之前所述中重要的几点做一个总结:
- 每一个函数都有一个
prototype
属性,指向它的原型对象。 - 这个原型对象上的所有属性和方法都会被它的构造器函数的实例对象继承。
- 任一对象的
__proto__
属性都指向它的构造器函数的原型对象。
数组的原型链
我们先来拿数组举个栗子。数组也是对象类型的数据,所以每一个数组都具有 __proto__
属性,我们来验证一下上面的第三条:
const arr = [1, 2, 3, 4];
console.log(arr.__proto__ === Array.prototype); // true
其实,这是因为上面的数组声明方式其实相当于:
const arr = new Array(1, 2, 3, 4);
所以,arr
是构造函数 Array
的一个实例对象并且我们已经验证了第三点。即它的 __proto__
属性指向 Array
的原型对象。
那么我们就可以根据第二点得出这一数组会继承 Array
的原型对象上的属性和方法,我们先来看一下 Array.prototype
上面究竟有什么属性和方法。
console.log(Array.prototype);
可以看到:
这里只是截取了部分属性方法,我们看到了熟悉的 concat
、filter
、indexOf
等等方法,并且看到了一个属性 length
。
看到这里大家就懂了为什么我们只是声明了一个数组,为什么它上面就有那么多的属性和方法呢。其实这都归功于 javascript
本身的原型与原型链机制。
到这里,我还迟迟没有推出原型链这一特性,接下来,就让我们沿着数组的原型链去一探究竟。
我们知道 arr.__proto__
其实就是 Array.prototype
。那么 arr.__proto__.__proto__
就应该是 Array.prototype.__proto__
。
我们来分析一下 Array.prototype.__proto__
:
- 首先
Array.prototype
可以看做数组的原型对象,那么它就是一个拥有键值对的对象。 - 又有一个简单对象的
__proto__
属性指向Object.prototype
。 - 可以猜测
Array.prototype.__proto__
就是Object.prototype
。
验证一下:
Object.prototype === Array.prototype.__proto__; // true
由于继承的传递性,可以得到每一个数组也都拥有 Obejct.prototype
这一原型对象上的属性和方法。
我想当你看到 toString
、valueOf
这些方法的时候你就已经一目了然了,原来一个对象上的这些方法都继承来自 Obejct.prototype
,不过有些原型对象上的这些方法被改写了而已。
当我们都以为一切都已经到头了的时候,我们发现 Object.prototype
这一原型对象也具有它的 __proto__
属性,这也应证了我说的每一个对象都具有这个属性:
展开看一下:
可以看到它上面有四个简单的属性:arguments
、caller
、length
、name
,熟悉函数的同学肯定知道这不是每一个函数上面应该具有的属性吗。
这究竟又是怎么回事呢?我们在讨论对象,怎么又跟函数扯上了关系。之前已经说了,所有的函数本质上都是对象,那么 Object
这一构造函数也不例外,它也是通过 new Function
出来的。所以有 Object.__proto__ === Function.prototype
:
讨论到这里后,我们回到 Person
的例子,Person
这一构造函数也具有原型对象,这一原型对象也是 Object
构造函数的实例对象。
所以有 Person.prototype.__proto__ === Object.prototype
这一结论是成立的。
那么重点来了,Function
也是一个对象,它和 Object
之前有什么关系呢?
首先排除掉 Function
是通过 new Object
出来的这种可能性。因为 Person.prototype
是通过 new Object
出来的,又有Person.__proto__ === Function.prototype
。
那么 Function.__proto__
到底指向谁呢?其实这里就是 javascript
语言本身的原型闭环。直接上答案:Function.__proto__ === Function.prototype
。可以看到这时非常特殊的。
距离原型链的顶端 null
就只有两部之遥了,加把劲,我们就要彻底搞清了。
Function
的原型对象也是一个对象,所以这个原型对象也是 Object
的实例,就有:Function.prototype.__proto__ === Object.prototype
。
那么再往上找,Object
的原型对象的 __proto__
就指向 null
了,因为它不可能是 new Object
获得的。这样,整个原型与原型链的机制我们就都摸清楚了。
我将上述讲解到的内容做出以下图解,相信你可以理清楚其中所有的脉络:
instanceof
学完整个原型链后其实我们也学会了一个运算符 instanceof
。
instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上,它的用法很简单,大家可以自行学习,这里继续本文的主题。
趁热打铁,我们来探索一下原型与原型链在社区中的优秀用例吧。
社区中的使用
关于原型与原型链在社区中的使用很多,这里我挑选出了两个大家耳熟能详的库和框架中的案例,jQuery
和 Vue
。
先让我们来看看在 jQuery
中是怎么巧妙地使用原型机制的。
jQuery
jQuery
相信我们每一个人都用过,最早也是它将原型链整个机制发扬光大的。
不知道你有没有发现这样一个现象,就是当我们通过 $()
创建出来一个 jQuery
的实例对象的时候,浏览器的控制台中显示的是这样子的:
它其实是 jQuery.fn.init
这一构造函数的实例对象,是不是很纳闷,让我们带着疑惑走进 jQuery
的源码进而探究其对原型机制的使用。
我们这里仅仅展示一个简单的架子,根据上面的结果,有下面的推测:
(function(window) {
var jQuery = function(selector) {
return new jQuery.fn.init(selector);
};
jQuery.fn = jQuery.prototype = {
css() {},
html() {},
each() {}
};
jQuery.fn.init = function(selector) {
};
window.jQuery = window.$ = jQuery;
})(window);
相信我们都可以看懂这几行代码吧,这里简单的解释一下:
- 首先整个代码放入一个自执行函数中,避免污染全局变量。
- 内部声明一个名为
jQuery
的函数,最后像全局window
上挂载jQuery
和$
供外部使用。 - 由浏览器中看到的返回的实例对象其实是
jQuery.fn.init
得到jQuery
内部其实是new jQuery.fn.init()
来创建实例对象。这里jQuery.fn
其实是jQuery.prototype
的一个引用。 - 之后在
jQuery
的原型对象上添加了很多供外部使用的方法,例如css
、html
等等。
解释完后疑问就随之产生了,为什么我们给 jQuery
的原型对象上添加的方法,jQuery.fn.init
的实例对象却可以访问到呢?
正当我们百思不得其解的时候,下面这行代码给了我们答案:
jQuery.fn.init.prototype = jQuery.fn;
这行代码是让jQuery.fn.init
的原型对象指向了 jQuery
的原型对象。这样一来 jQuery.fn.init
的实例对象就可以拿到 jQuery
的原型上的方法,这里给出图解:
这一原型共享的设计可谓是十分的巧妙。
Vue
了解了 jQuery
中的共享原型后,我们再来探索一下在 Vue2.x
中的代理原型的设计。
在 Vue2.x
的响应式系统中,为了拦截可以改变数组本身的 7
个操作:push
、pop
、unshift
、shift
、splice
、reverse
、sort
。设计出了代理原型的模式来进行拦截。
我们来探究一下其内部是怎么做的:
const arrayProto = Array.prototype;
const proto = Object.create(arrayProto);
[
'push',
'pop',
'unshift',
'shift',
'splice',
'reverse',
'sort'
].forEach(method => {
proto[method] = function(...args) {
console.log(`拦截到了${method}方法的执行`);
arrayProto[method].call(this, ...args);
}
});
const arr = [1, 2, 3];
arr.__proto__ = proto;
arr.push(4);
console.log(arr);
我们来解析一下以上代码:
- 首先
arrayProto
拿到数组原型对象的引用。 - 创建一个
proto
的对象,继承自数组的原型对象,此时proto
上继承了所有数组原型对象的属性和方法。 - 接着遍历准备好的包含七个方法名称的数组,依次重写
proto
上的这些方法。 proto
对象上对应的这七个方法分别进行了一次拦截操作,并且内部其实继续使用call
绑定this
指向调用了数组原型上的对应的方法。- 接着我们创建了一个数组,修改其
__proto__
属性指向新创建的对象,之后调用push
方法。
可以在浏览器中看到:
我们对对应的方法进行了拦截,并且没有破坏数组原来的方法的作用(这里指添加一个元素)。
这就是 Vue2.x
中对原型的高级使用。
使用原型
在了解了优秀的类库或者框架中对原型机制的使用后,我们在日常代码中可以怎么使用这一机制来优化我们的代码呢?
这里我将给大家举个例子,例如我们要实现一个数组的去重方法,我们可以这样设计代码:
Array.prototype.unique = function() {
return Array.from(new Set(this));
};
之后你只需要这样使用:
const arr = [1, 2, 3, 1, 2];
console.log(arr.unique()); // [1, 2, 3]
当然你可以将你的 unique
设计的更为强大,例如可以添加一个回调函数等。
但是有一个需要注意的是,如果你想要在项目中使用此类方法,一定要确保这以后不会称为 javascript
的语言规范,不然对于一个长期使用的项目来说会是一个很大的麻烦。例如,上面的 unique
函数以后成为了语言规范(这里只是假设),在你的项目中后面来的新人在维护项目的时候使用了这个方法。因为他不知道你已经修改了原型上的这个方法,会对他们的使用造成很大的困惑。所以我们在项目中使用的时候要注意命名,尽量避免这类问题。
总结
有关 javascript
语言中的原型与原型链机制这里就介绍完毕了,相信弄清楚上面的知识后,有关这部分的问题对你来说再也不会是问题了,你也不需要再回过头来一次又一次的似懂非懂的学习。
将一个知识点从使用到原理解读、底层实现、社区如何使用、最后到自己实战中使用经历这些步骤后肯定会对它不再畏惧。最后将所有的知识点总结起来,关联起来就可以形成一个完整的知识体系。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!