文章重点在于知识串联, 理解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生成子类原型对象, 因此具有原型模式生成对象的缺点。
- 借用构造函数
在父类生成实例时,改变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指向进行了变更,使其指向待生成实例)。
该方法借'父类'构造函数,生成自己的实例,且使得实例只继承自身。
缺点在于构造函数本身的方法无法复用的问题。
- 组合继承(经典继承)
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
对于1、 2 封装一下
function inherit(Person, Man){
const prototype = Person.prototype
Man.prototype = prototype
Man.prototype.constructor = Man
}
inherit(Person, Man)
渐进式的优化,到这里已经很完美了
看到这里,谢谢你耐心观看!
第一次手敲这么长的文章,受限于经验和视野,如有错误,欢迎留言。