【Javascript】- 实例、原型、构造函数

203 阅读7分钟

刚接触 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