前端学习21 原型和原型链
原型和原型链都是来源于对象而服务于对象的概念,所以我们要明确一点:
JavaScript的一切引用类型都是对象,对象就是属性的集合。
1.原型和原型链
每一个对象从被创建开始就和另一个对象关联,从另一个对象上继承其属性,这个另一个对象就是原型。
当访问一个对象的属性时,先在对象的本身找,找不到就去对象的原型上找,如果还是找不到,就去对象的原型(原型也是对象,也有它自己的原型)的原型上找,如此继续,直到找到为止,或者查找到最顶层的原型对象中也没有找到,就结束查找,返回undefined。
这条由对象及其原型组成的链就叫做原型链。
- 原型存在的意义就是组成原型链:引用类型皆对象,每个对象都有原型,原型也是对象,也有它自己的原型,一层一层,组成原型链。
- 原型链存在的意义就是继承:访问对象属性时,在对象本身找不到,就在原型链上一层一层找。说白了就是一个对象可以访问其他对象的属性。
- 继承存在的意义就是属性共享:好处有二:一是代码重用,字面意思;二是可扩展,不同对象可能继承相同的属性,也可以定义只属于自己的属性。
2.创建对象
对象的创建方式主要有两种,一种是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
Object、Array等称之为构造函数,构造函数和普通函数没什么不同,只是由于这些函数常被用来跟在new后面创建对象。new后面调用一个空函数也会返回一个对象,任何一个函数都可以当做构造函数。
所以构造函数更合理的理解应该是函数的构造调用。
Number、String、Boolean、Array、Object、Function、Date、RegExp、Error这些都是函数,而且是原生构造函数,在运行时会自动出现在执行环境中。
构造函数是为了创建特定类型的对象,这些通过同一构造函数创建的对象有相同原型,共享某些方法。举个例子,所有的数组都可以调用push方法,因为它们有相同原型。
3.__proto__和prototype
上面我们说过,每个对象都有原型,我们可以通过对象的__prototype__属性指向对象的原型。
引用类型皆对象,所以引用类型都有__proto__属性,对象有__proto__属性,函数有__proto__属性,数组也有__proto__属性,只要是引用类型,就有__proto__属性,都指向它们各自的原型对象。
__proto__属性虽然在ECMAScript 6语言规范中标准化,但是不推荐被使用,现在更推荐使用Object.getPrototypeOf,Object.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() // 输出:喝东西
所以对于__proto__和prototype:
- 对象有__proto__属性,函数有__proto__属性,数组也有__proto__属性,只要是引用类型,就有__proto__属性,指向其原型。
- 只有函数有prototype属性,只有函数有prototype属性,只有函数有prototype属性,指向new操作符加调用该函数创建的对象实例的原型对象。
4.原型链顶层
原型链之所以叫原型链,而不叫原型环,说明它是有始有终的。
// 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)的链就是原型链。
5.constructor
构造函数都有一个prototype属性,指向使用这个构造函数创建的对象实例的原型对象。
这个原型对象中默认有一个constructor属性,指回该构造函数。
Person.prototype.constructor === Person // true
6.函数对象的原型链
之前提到过引用类型皆对象,函数也是对象,那么函数对象的原型链是怎么样的呢?
对象都是被构造函数创建的,函数对象的构造函数就是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__属性指向Function的prototype属性。
7.神图!!!
8.继承?
前面我们讲到每一个对象都会从原型“继承”属性,实际上,继承是一个十分具有迷惑性的说法,引用《你不知道的JavaScript》中的话,就是:
继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性,所以与其叫继承,委托的说法反而更准确些。