刚接触 Javascript ,原型、原型链、构造函数这些概念傻傻分不清楚? 这篇文章就带你好好捋一捋
构造函数
最开始,在 Javascript 中,使用 “工厂模式” 来创建对象,这种方式简单易懂,非常直观。
它的原理是:创建一个函数,函数内部再创建一个空对象,然后将接受的参数全部注入这个空对象作为空对象的属性,然后返回这个对象
function Person(name, age) {
const obj = {}
obj.name = name
obj.age = age
obj.say = function () {
console.log('my name is:', obj.name)
}
return obj
}
const Aelly = Person('Aelly', 28)
console.log(Aelly instanceof Person) // false
这种方法虽然简单且容易理解,但是它存在缺点:无法识别实例对象的具体类型,会重复创建功能相同的方法,所以最好不要使用工厂模式来创建对象
在 Java 中,都是先创建一个类,然后根据这个类来实例化对象。但是 Javascript 中没有类这个概念,后来 Javascript 提供了构造函数来实现对象的实例化。
构造函数赋予了对象初始的属性和方法,就像小孩子刚出生时已经有手有脚了
function Person(name, age){
this.name = name
this.age = age
this.say = function(){
console.log('my name is:',this.name)
}
}
// 实例化对象
const Jack = new Person('Jack', 28)
上面的代码演示了通过构造函数实例化对象的标准用法,Person 就是一个构造函数,Person 内部有一个 this 对象,Person 将接受的参数全部绑定到 this 上,然后返回这个 this 对象。但是在代码中,并没有看见 return 语句,这都是因为 new 操作符的作用。
一定要对构造函数使用 new 操作符才能实例化对象
前面提到了用工厂模式实例化对象,其实构造函数的原理和工厂模式一模一样:创建一个空对象,然后将函数参数作为空对象的属性注入,最后返回对象。
只不过 new 操作符将这一系列操作封装起来了,这样写法更简便!最重要的是,构造函数实例化的对象可以识别其确切的类型,代码如下:
function Person(name, age){
this.name = name
this.age = age
this.say = function(){
console.log('my name is:',this.name)
}
}
const Jack = new Person('Jack', 28)
console.log(Jack instanceof Person) // true
原型
使用构造函数实例化对象虽然可以识别对象的类型,但是它也存在缺点:还是会重复创建功能相同的方法,每当对象实例化一次,构造函数中的方法就会被创建一次,要知道每个方法都一模一样,但是却要占用新的存储空间,会造成不必要的浪费。
函数本质上也是对象,我们创建的任何一个函数都会有一个 [[prototype]] 属性(ECMAScript规定的),它是一个指针,指向一个对象——原型对象(prototype)
我们使用构造函数实例化的每个对象都会继承这个原型对象;它包含的则是实例对象共享的属性或方法
function Person(){}
Person.prototype.name = "Jack"
Person.prototype.age = 29
Person.prototype.say = function(){
console.log(this.name)
}
const person1 = new Person()
const person2 = new Person()
person1.say()
person2.say()
把方法放进原型对象里,然后每个实例化的对象都会拥有这个方法的引用,也就是说,一次创建,多处共享。上面代码中,对象 person1 和 person2 都会拥有 say() 方法,而且,是同一个内存地址中的同一个 say() 方法
然而新的问题又出现了, prototype 是一个对象,它不能像函数一样接受参数;假如实例化 100 个 Person,每个对象代表一个人,那么每个人的名字都会是 jack,这样显然不行。 于是新的方法又出现了
双剑合璧
构造函数的优点是接受自定义的参数作为对象的属性,缺点是会创建重复的函数;原型模式的优点是“一次创建,多处共享”,缺点是不能接受自定义的参数,会创建属性完全相同的对象。有没有发现,这两者正好优缺互补!
将对象之间需要共享的属性或方法放到原型中,而使用构造函数来接受对象中需要自定义的属性或方法,这样创建出来的对象,可以说是完美了,于是我们有了下面的代码:
function Person(name, age){
this.name = name
this.age = age
}
Person.prototype = {
constructor: Person,
say: function(){
console.log("my name is: ", this,name)
}
}
const Jack = new Person('jack', 28)
Jack.say() // my name is: jack
const Aelly = new Person('Aelly', 28)
Aelly.say() // my name is: Aelly
原型和构造函数之间的关系
我们先来打开一个对象,看看里面到底有些什么:
function Person(name, age){
this.name = name
this.age = age
}
Person.prototype = {
constructor: Person,
say: function(){
console.log("my name is: ", this,name)
}
}
const Jack = new Person('jack', 28)
console.log(Jack)
打印的结果:
我们可以看到,age 和 name 属性理所应当的出现了,但是我们却没有看到 对象 Jack 的 say 方法,而是多出了一个 [[prototype]] 属性,这是怎么回事?我们打开来看看里面有什么:
终于,我们看到了“消失的say 方法” ,原来它藏在 [[prototype]]属性里面;对了,前面说过,对象之间共享的方法和属性是定义在 prototype 对象里的,现在我们发现 say 方法是存在于对象 Jack 的 [[prototype]]属性中,由此我们可以得出结论: [[prototype]]属性是一个指针,它指向了对象的 prototype (原型)
好了,[[prototype]]属性我们已经弄清楚了,但是[[prototype]]属性里面还有一个 [[prototype]],而且又多了一个 constructor 属性,好吧,再来打开看看:
constructor 属性
[[prototype]] 属性里面的那个 [[prototype]]
通过打开 constructor 属性,我们可以发现:这不就是构造函数 Person 吗!是的,constructor 也是一个指针,它的指向就是构造函数
我们打开了[[prototype]]属性里面的那个[[prototype]],然后发现,谢天谢地,终于没有再出现[[prototype]]了!为什么呢?后面会解释这个现象
现在我们来总结一下原型、构造函数、实例之间的关系:
- 构造函数有一个
prototype属性,指向构造函数的原型对象 - 原型对象有一个
constructor属性,它指向了构造函数
- 每个实例化的对象都有一个
[[prototype]]属性,它指向当前对象的原型对象
原型链
我们已经知道了原型、实例、构造函数之间的关系,但是我们还不知道,实例是如何查找到定义在原型中的方法或属性的。所以,先来看一段代码:
const a = {
attr1: 'a'
};
const b = {
attr2: 'b'
};
Object.setPrototypeOf(b,a)
const c = {
attr3: 'c'
};
Object.setPrototypeOf(c,b)
console.log(c.attr1) // 'a'
我们要打印的是对象 c 的 attr1 属性,但是 c 中并没有定义这个 attr1 属性,可是还是能打印出结果。那么问题来了:c 中这个 atrr1 是哪来的?
在代码中,我们通过Object.setPrototypeOf()将 b 的prototype显式地赋值为对象 a ,也就是将 b 的原型指定为 a ,同样的,对 c 也执行相同的操作,将prototype指定为 b 。我们打开 c 来看看:
如果你看懂了上面的截图,那你已经明白了本文最重要的东西——原型链
什么是原型链?c 中没有 attr1 这个属性,但是它的[[prototype]]指向 b ,于是 c 就打开[[prototype]]到 b 中查找这个 attr1 ,但是 b 中只有 attr2 没有 attr1,于是 b 又打开了它自己的 [[prototype]],到 a 中查找,终于找到了 attr1;于是 attr1 的引用就被返回给 c ,c 成功的访问到了 a 中的 attr1。
这完全是一个链式查找的过程,从当前对象开始一级一级的向上查找(通过[[prototype]]属性),链的终点就是 Object——所有对象的原型对象(前文[[prototype]]中终于没再出现 ****[[prototype]]的原因 )。同时,这个操作不知不觉的完成了面向对象编程中很重要的一件事——继承
现在你应该懂了,对象的实例化依靠构造函数和原型,而对象的继承则要靠原型链
补充
你一定见过一个叫做 __proto__ 的属性,它是浏览器内置的属性,为了方便的取到 prototype 对象而设置的,目前已经从浏览器(Chrome 98)中移除了。
它的缺点就在于,只有浏览器环境中才存在,而且从它命名前后加上了双下划线来看,你也能知道,它是一个内部的“不愿意轻易让人看见的属性”。
所以实际编写程序的时候,不要用 __proto__ 属性来获取 prototype 对象,ECMAScript标准也不存在这个 __proto__ 属性。
ES6提供了新的API用于获取 prototype 对象,它就是——Object.getPrototypeOf 方法
function Person(){}
Person.prototype = {
constructor: Person,
say: function(){
console.log("Hello World")
}
}
const person1 = new Person()
person1.__proto__ === Object.getPrototypeOf(person1) // true