一、前言
本篇文章的目的是尽可能的解释清楚在js运行时的原型链。我准备从三个问题着手:
- 原型是什么?
- 原型链是什么?
- 原型链的应用?
二、什么是原型?
- 在编写javascript代码过程中,经常会用到
Array
、Object
、Function
等构造器创建对象。如下:const obj1 = {} const obj2 = new Object() console.log(obj1.toString) // [object Object] console.log(obj2.toString) // [object Object]
- 如上,在创建
obj1
时,我们并没有给它添加toString
方法。但是它却能够使用toString
方法。这是为什么呢?这是因为toString
方法是在Object
的原型上,而在创建obj1
时,会将Object
的原型挂载到obj1
的原型链上。Object
的原型是什么呢,可以通过浏览器控制台console.log(Object.prototype)
打印出来。如下:console.log(Object.prototype) console.log(typeof Object.prototype) // object console.log(Object.prototype === obj1.__proto__) // true // constructor: ƒ Object() // hasOwnProperty: ƒ hasOwnProperty() // toLocaleString: ƒ toLocaleString() // toString: ƒ toString() // ....
- 每一个构造方法都有一个
prototype
属性,它构造的每一个对象,都会有一个__proto__
属性,这个**__proto__
属性就是原型**。而且原型也只是一个普通的对象。在javascript中,每一个对象,都有自己的原型。万物皆对象,指的就是所有的数据类型,都有一个共同的原型,Object.prototype
。所以他们都是Object构造方法创建的。
三、什么是原型链
-
通过上面的例子,可以知道,所有的对象,都有一个共同的原型,那就是
Object.prototype
。但是,Object.prototype
的方法却不多,而在实际开发中,有很多方法都是Object.prototype
中没有,本身也没声明,但是却可以用的,比如:const arr = [] arr.splice(0, 1, 1) console.log(arr) // [1] console.log(arr.toString) // 1
-
在执行上述代码中,
arr
对象不仅仅调用了Object.prototype
原型中的方法,还调用了一个splice
方法,这个在原型跟创建对象时都没有声明的,但他却执行了,这是为什么呢?尝试找一找原因:console.log(arr.__proto__) // concat: ƒ concat() // constructor: ƒ Array() // copyWithin: ƒ copyWithin() // splice: ƒ splice() // .... console.log(arr.__proto__.__proto__ === Object.prototype) // true console.log(arr) // [1] console.log(arr.toString) // 1
-
通过上述代码的执行,可以发现,原型并不只是单独的某一个对象,它可以是多个对象一级一级通过
__proto__
属性串联起来,形成一个链式的结构,而splice
方法,正是在此条链上可以找得到,这就是js中的原型链结构。js在获取对象属性时,会先查找当前对象有没有该属性,如果无,则查找__proto__
指向的对象是否有,在__proto__
对象上继续此操作,直到找到该属性或者__proto__
为null
为止。而在此例中,toString
方法是在arr.__proto__.__proto__
中找到,而splice
则是在arr.__proto__
找到。 -
原型链查找流程图
四、原型链的应用
1、实现类与类的继承
javascript原本是基于原型的语言,原本是没有类的概念,但是由于一些历史原因,在初期为了推广,仿照java语言,在基于原型的基础上,又引入了this,new等语言特性,能通过一些妥协基本实现类的功能。
this
this关键字,是在运行时全局作用域与函数作用域下可以直接访问的对象。
- 全局作用域下(浏览器环境):
window
。 - 方法调用: 执行该函数时调用的对象。
- 普通函数执行:
window
。 - 严格模式非方法调用:
undefined
- 如浏览器控制台执行如下代码:
console.log(this) // window const obj = { print: function() {console.log(this)}, strictPrint: function() { "use strict" console.log(this) } } const print = obj.print const strictPrint = obj.strictPrint print() // window obj.print() // obj strictPrint() // undefined obj.strictPrint() // obj
- 通过打印可以看出,
this
的初始化在运行时创建作用域时完成,按照一定规则赋值的一个对象。所以this
也会有自己的原型链,而js中的类,可以利用this
的原型链特性,实现类的继承。
new
new
关键字,一般用来创建类的实例,但是在js中,并没有类的概念。但是new
,还是通过一些转变,实现了类似的功能。如下:
function Dog(name) {
this.name = name
this.say = function() {console.log(this.name + ':汪,汪,汪~'+this.type)}
}
const dog1 = new Dog('小黑')
const dog2 = new Dog('大黄')
dog1.say() // 小黑:汪,汪,汪~undefined
dog2.say() // 大黄:汪,汪,汪~undefined
function Husky(name) {
this.name = name,
this.type = 'Husky'
}
Husky.prototype = new Dog() // 绑定原型链
Husky.prototype.constructor = Husky // 还原构造函数
const husky1 = new Husky('小黑')
const husky2 = new Husky('大黄')
husky1.say() // 小黑:汪,汪,汪~Husky
husky2.say() // 大黄:汪,汪,汪~Husky
- 上述代码实现了一个简单的类以及类的继承。
但是,要注意的是,基于原型的继承。在运行时如果改变了原型链上的方法。那么原型链的所有分支都将收到影响。因为原型链仅仅是模仿了继承,如果要实现更高级的继承。那么需要额外引入一些方法。比如使用方法对象的call,apply等方法,将继承对象的属性挂载到当前对象上。
- 上述代码中,
new
实际是js帮忙封装了几步。new
的实际操作用代码拆分如下function Dog(name) { this.name = name this.say = function() {console.log(this.name + ':汪,汪,汪~'+this.type)} } function newDog(name) { // 创建一个新对象 const obj = {} // 挂载原型链 obj.__proto__ = Dog.prototype // 利用Dog方法对象原型链上的call方法,重新指定执行是this对象为obj Dog.call(obj, name) return obj } const dog = newDog('大黄') dog.say() // 大黄:汪,汪,汪~undefined
- 在开发中,如果偏爱使用类进行编码的,可以通过以上方式,利用原型链实现类似的功能
2、拓展内置对象
Array数组的方法
- 如果有一个需求,需要在运行时,在多个模块或地方要对数组对象进行定制化操作,有什么方法可以快速实现这个需求呢?
- 有的,比如,假设需要对数组中的每一个
number
类型的项进行一次自增,因为所有的数组实例,都有共同的原型为Array.prototype
。所以可以将此方法挂载到Array.prototype
上,就实现了所有数组实例都能直接调用该方法了。如:
Array.prototype.add = function() {
this.forEach((item, index) => {
if(typeof item === 'number') this[index] = ++item
})
return this
}
const arr = [1,2,'aaa']
console.log(arr.add()) // [2, 3, "aaa"]
- 当然,也可以重写数组的内置方法,vue中就是这么实现数组的数据侦听的。
以上就是本人对原型链的理解跟总结了,如有不足不实之处,欢迎各位大佬批评指点~