对象、原型、原型链与继承

文章重点在于知识串联, 理解JS中的对象创建对象的方法,以及这些方法背后的原理,如原型链,new操作符、this指向的变更等知识
适合读者:
1. 对原型链懵懵懂懂的新手
2. 对以上知识点有所了解但并未串起来的入门玩家

一、对象

javascript中的对象定义为:无序属性的集合,其属性值可以包含基本值、对象或者函数,可以理解为散列表。
问题一: 如何创建对象?

原始方式一:对象字面量
let person = {
	name : 'json',
	age:24,
	sayName: function(){
		console.log(this.name)
	}
}
原始方式二:Object实例:
const person = new Obejct()
person.name = 'json'
person.age = 24
person.sayName = function(){
	console.log(this,name)
}

上述两种方法创建单个对象可以,但创建多个对象时会产生大量重复代码
为解决这个问题,很多前辈做出了探索:

1) 工厂模式: 使用函数对上述两种原始方案做封装
function createPerson (name, age){
	const person = new Object()
	person.name = name
	person.age = age
	person.sayName = function(){
		console.log(this.name)
	}
	return person
}
评价: 有效解决了创建多个对象引起的代码冗余问题,但无法有效识别出对象(即判断对象是哪种类型,
如createPerson,createAnimal分别创建了person、animal两种对象,但他们类型均是Object,我们更希望
了解到改对象属于person或者animal)。

2) 构造函数模式:构造函数是函数,用来创建特定类型的对象,ESCAscript支持如Object、Function等原生构造函数,也允许自定义构造函数,通常函数名称首字母大写,如Person
function Person (name, age){
	this.name = name
	this.age = age
	this.sayName = function(){
		console.log(this.name)
	}
	//上述写法用字面量的方式定义sayName函数,这样写可能会更加容易理解
	//this.sayName = new Function(){
	//	console.log(this.name)
	//}
}
调用:let person = new Person('json', 24)
评价:使用特定的构造函数创建特定的类型,有效解决了对象识别问题。但是每调用一次构造函数生成实例,就会创造出一个sayName函数。

补充几个知识点


1)this 是什么?
一言以蔽之,this是函数调用时产生的对象,该对象绑定函数调用时所在执行环境。
函数在全局环境下执行时,this指向window对象;作为对象的方法执行时,this指向于该对象。

var name = 'global this'
function sayName (){
	return this.name
}
var obj = new Object()
obj.name = 'personal this'
obj.sayName = sayName

console.log('window', sayName())
console.log('person', obj.sayName())

注意:name属性声明时,使用var声明, 当使用let声明时,sayName()调用返回值为undefined

更多关于this的知识参考:
阮一峰老师的文章:
this原理:http://www.ruanyifeng.com/blog/2018/06/javascript-this.html
this的用法:http://www.ruanyifeng.com/blog/2010/04/using_this_keyword_in_javascript.html
言归正传,在构造函数模式中,定义了this.name = name等属性,在实例化构造函数的时候,this指向于构造出的实例,因此对象上会有name等属性。**那么,构造函数时怎么创建实例的呢? **


2)使用new操作符生成实例

对于上述定义的Person构造函数,使用如下方法生成实例
let person = new Person(name,age)
这样生成的实例,使用了对应的name,age属性,且可以分辨出该对象的类型

为什么new一下就可以了?
new操作符进行了如下过程:
a)创建一个新的对象
b)将该对象挂载到构造函数的原型上
c)使构造函数的this指向自身,且调用构造函数生成对应的属性和方法
d)返回该对象
//自己实现一个new方法
function myNew (constructor = window, ...args){
	let obj = new Object()					//对应a)
	obj.__proto__ = constructor.prototype	//对应b)
	let res = constructor.apply(this, args) //对应c)
	return typeof res === 'object' ? res : obj //对应d)
}

解释:b)过程描述与红宝书145页描述有区别,主要在于红宝书的(2)-(3)步骤合成为c)步骤,b)步骤完成原型链的挂载,因此实例可以被识别(instanceof方法)
d)过程对返回值类型做判断,new操作符返回类型一定是对象,但并非所有构造函数均有返回值,如Person,若没有返回值,则返回我们构建的对象

这下好了,我连this都没弄明白,又多出来了个apply、原型链
莫慌,我们一个一个掰扯清楚!


3)apply是什么?又做了什么?

function Person (name, age){
	this.name = name
	this.age = age
	this.sayName = function(){
		console.log(this.name)
	}
}
有这么一个需求,有一个dog对象,也可以有一个sayName方法
你可能会这么写
let dog = {
	name = 'dog'
	sayName = fucntion(){
		console.log(this.name)
	}
}
这么写对吗? 对, 但与Person中的sayName方法重复定义,有没有办法能在不定义该方法的前提下使dog对象也用到Person类中的方法呢?有!哒哒哒, 就是apply
Person.sayName.apply(dog)
apply方法改变函数调用时this的指向,指向于传入的第一个参数,相当于obj.sayName()
类似作用:call
可比较方法:bind
实现一下吧:
a) 在绑定对象上创建方法
b) 调用该方法生成结果
c) 删除该方法
d) 返回结果
function myApply(context = window, ...args){
	context,fn = this				a)
	let res = context.fn(args)		b)
	delete context.fn				c)
	return res						d)
}
补充:
1)apply方法与call方法,效果相同,均为改变this指向,返回该方法调用后结果。差别在于apply接受数组作为第二参数,而call方法接受多参为参数,可尝试实现
2) bind方法也可改变this指向,返回指向变更后的函数。相对于以上两种方法,bind实现较复杂,文末讨论

3)原型与原型链
原型(prototype)是什么?
原型是对象,该对象伴随构造函数产生。

浏览器中输入:
	function Person (name){ },
 然后我们看一下Person.prototype
![prototype](https://img-blog.csdnimg.cn/20190503160424632.png)
该对象中有constructor属性,属性值为Person
透过原型可以了解实例是如何被创建的,且这里我们可以get到一种类型判断的方法:
(1) instanceof方法
简单实现
function instanceof(obj, constructor){
	const prototype = constructor.prototype
	while(obj.__proto__!== null){
		if(obj.__proto__ === prototype){
			return true
		}
		obj = obj.__proto__
	}
	return false
}

那么, **proto**又是什么?

____称为隐式原型,每个对象均有该属性,指向创造该对象的构造函数的显式原型(prototype),可理解为继承自该原型
let obj = {}
obj.__proto__ === Object.prototype // true
__proto__在对象创建时产生,作用可能就是了解到它的原型是哪个吧。。。。(不太了解用啥用。。。)

构造函数、原型与实例的关系:

prototype 和__proto__构成原型链

1、每个对象均有__proto__属性,指向该对象构造函数的protoytpe
2、函数具有prototype属性,该属性为对象,对象中定义constructor属性,属性值为该构造函数
3、javascript内置的构造函数如Function、Object、Array等。
作为对象,由Function构造函数实例化创建,因此其__proto__均指向Function的原型
如Array.__proto__ == Function.prototype
构造函数的原型(prototype)是对象,最初的对象是从哪里来的?
Function构造函数的__proto__是函数,最初的函数是从哪里来的?
均是由浏览器创造的。
浏览器创造Object和Function的原型对象,
并使Function.prototype的隐式原型指向Object.prototype。Function.prototype.__proto__ == Object.prototype 
Object的隐式原型指向null,Object.prototype.__proto__ == null。
这点鸡和蛋的问题参考:https://zhuanlan.zhihu.com/p/62808138
4、原型链作用在于属性的查找。当在对象中查找某一个属性时,首先在当前对象中查找,找到则返回,找不到则沿着原型链向父级原型查找,直到查找到null后返回。
这块儿可以延申的知识点也很多,如数据属性、访问器属性,遍历对象属性的方法,属性改写等,不做具体介绍。

在这里插入图片描述
3)原型模式创建对象

function Person (){}
Person.prototype = {
	constructor:Person,
	name : 'json',
	age  : 24,
	friends : ['js', 'ajax', 'css', 'html']
}
let person = new Person()
这里我们构建了空构造函数Person,并改写了其原型。
之后通过new的方式,创造实例,那么该实例的隐式原型指向其构造函数的原型,即我们改写的对象。
当我们在对象中访问属性时,当前对象中不具有任何属性(因为构造函数中没有生成任何属性),因此会沿着原型链查找,到Person.prototype上查找属性。

那么,问题就来了! 当Person.prototype上的属性为引用类型时,通过某一子实例访问其原型中引用属性,且对其修改时,该变化直接发生在其原型上,其他实例在访问该属性时也可观测到对应变化。

let person2 =  new Person()
person2.friends.shift()
person1.friends//['js', 'ajax', 'css']

该模式在创建对象时,不能传递参数,且属性中存在引用类型时,修改会被其他实力共享。

4)组合模式(原型+构造函数)
构造函数模式可以传递参数,但内部定义方法在每个实例创建时均会重复构建,可结合原型模式取长补短。

function Person(name, age){
	this.name = name,
	this.age = age
}
Person.prototype.sayName =  fucntion (){
	console.log(this.name)
}
使用构造函数在实例创建时进行属性构建,使用原型进行方法共享

但是,这个看起来封装的不太好,那就改造以下

function Person(name, age){
	this.name = name,
	this.age = age
	if(this.sayName !== function){
		Person.prototype.sayName =  fucntion (){
			console.log(this.name)
		}
	}
}

以上简单了解了对象、创建对象的方法,接下来瞧一瞧如何javascript中的继承是如何实现的

引用红宝书中的继承概念(p162):
ECMAscript只支持实现继承,而实现继承主要是依靠原型链完成的。

继承实际上完成使得一类对象具有另一类对象的属性和方法的任务,结合原型链可以这样做。
1)原型链

//父类
function Person(){
	this.skinColor = 'yellow',
	this.attributes = ['speak','think','make tools']
}
Person.prototype.sayName = function(){
	console.log(this.name)
}
//子类
function Man (){}
Man.prototype = new Person()
let man1 = new Man()
console.log(man1.skinColor)

首先创建父类Person,并内置属性skinColor, attributes
创建空子类,并改写子类原型为父类的实例
当子类实例查找属性时,首先在该实例中查找(此时为空),然后沿着原型链在Man.prototype中查找。Man.prototype由Person实例化产生。回顾以下new方法,首先会将生成的实例挂载到原型链中,然后调用父类构造函数方法生成自身属性,最后判断后返回。因此,man1为Man实例,Man继承自Person。

该方式使用new Person生成子类原型对象, 因此具有原型模式生成对象的缺点。
  1. 借用构造函数
在父类生成实例时,改变this指向,使其指向子类实例
function Person (name, age){
	this.name = name,
	this.age = age
	this.sayName = function(){
		return this.name
	}
}
function Man(name, age, skills){
	Person.apply(this, [...arguments])
	this.skills = skills
}
let man1 = new Man()
console.log(man1.sayName())

new方法在调用时,将待生成实例挂载到Man的原型链上,
然后调用Man构造函数方法,为实例构建属性,此时改变Person方法调用的时this指向,使其指向子类待生成实例(new方法中也对this指向进行了变更,使其指向待生成实例)。

该方法借'父类'构造函数,生成自己的实例,且使得实例只继承自身。
缺点在于构造函数本身的方法无法复用的问题。
  1. 组合继承(经典继承)
function Person(name){
	this.name = name
}
Person.prototype.sayName = functions(){
	console.log(this.name)
}
function Man(name, age){
	Person.call(this, ...arguments) //父类只在构造函数中定义属性,则此处只构造实例属性
	this.age = age
}
Man.prototype = new Person()//改写原型为父类实例,该实例只挂载了方法
Man.prototype.constructor = Man//对改写的原型添加constructor属性,使其指向自身
let man1 = new Man('json')
man1.sayName()
首先构建父类的属性和方法,子类通用解用父类构造函数生成实例属性
然后将子类原型改写为父类实例,这样当子类实例调用该方法时,首先在实例本身查找该方法,查找不到后沿原型链向父类原型查找,会找到对应方法。细节就是改写了原型后需要将原型的constructor置向该构造函数

上边的几种方法如果你掌握了,那就很厉害了。以下补充一些继承方法


4)寄生继承

Object内置了create方法,该方法可以根据给定的实例,生成新的实例,新实例原型为给定实例
模拟实现:
funtion myCreate(obj){
	function F(){}
	F.prototype = obj
	return new F()
}
在生成的实例中查找属性时,实际上是到F.prototype中查找,即obj对象上查找

寄生继承
function Person(obj){
	let res = myCreate(obj)
	res.sayName = function(){	
		console.log('json')
	}
	return res
}
这种方式只是对于创造对象的封装,首先创造对象,然后定义对象属性,最后返回对象

5)组合继承+寄生继承

组合寄生实际上已经很完美了,残缺的部分在于
1、改写子类原型时候第一次调用父类构造函数
2、继承父类属性的时候再次调用父类构造函数
仔细想一想,第一次调用父类构造函数只是为了产生父类原型的副本(通过实例可以找到父类的方法),那么这次调用可以使用myCreate方法完成,产生父类构造函数原型的副本。
function Person(name){
	this.name = name
}
Person.prototype.sayName = functions(){
	console.log(this.name)
}
function Man(name. age){
	Person.call(this, ...arguments)
	this.age = age
}
Man.prototype = myCreate(Person.prototype) 	// 1
Man.prototype.constructor = Man				// 2
对于12 封装一下
function inherit(Person, Man){
	const prototype = Person.prototype
	Man.prototype = prototype
	Man.prototype.constructor = Man
}
inherit(Person, Man)
渐进式的优化,到这里已经很完美了

看到这里,谢谢你耐心观看!
第一次手敲这么长的文章,受限于经验和视野,如有错误,欢迎留言。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值