构造函数和原型链相关
面向对象和原型链应该是很明朗的一个知识点了,本文希望从设计以及理解的角度,把它们讲清楚。
为什么函数是一等公民,或者说它有何特殊之处?
我们都知道js中有基本数据类型和引用类型。typeof基本类型可以拿到自己的类型,null比较特殊,可以理解为一个空的对象。而typeof引用类型(对象)就比较有意思了。typeof
数组,时间,正则等返回的都是object,唯独函数,返回的是function
。那么为什么fuction会这么特殊呢?其实function和其他对象最大的区别就是function是可以创建对象的,它和object有点鸡生蛋和蛋生鸡的关系。从设计的角度也是比较好理解的,我们通过对象去解决各种问题。例如我们需要一个能喊出自己名字的对象。
var zhangsan = {name:'zhangsan',sayName(){console.log(zhangsan.name)}}
var lisi = {name:'lisi',sayName(){console.log(lisi.name)}}
上面是创建两个对象,如果我们需要批量创建对象呢?那就搞一个能生产对象的对象吧,这个特殊的对象即函数。
function createPerson(name){
return {
name:name,
sayName(){console.log(name)}
}
}
var zhangsan = createPerson('zhangsan')
var lisi = createPerson('lisi')
构造函数和原型链是什么,二者有什么关系?
目前来看,有了能创建对象的函数,我们已经能实现批量创建对象了,这个功能很棒,不过考虑一下另外一个问题。如果有一些所有对象都会共有的方法或者属性,例如toString,valueOf等,我们需要每次创建新对象的时候都创建一遍吗?例如下面这样:
var anyObj={
toString(){},
valueOf(){}
}
function createPerson(name){
return {
//toString(){}
...anyObj,
name:name,
sayName(){console.log(name)},
}
}
var zhangsan = createPerson('zhangsan')
var lisi = createPerson('lisi')
function createAnyObj(){
return {
...anyObj,
selfFn...
}
}
// ...其他函数
显然每个函数都写一遍或者展开一遍是不合适的,浪内存费,处理复杂等等。这时候我们就要考虑这样一种模式了,在每个对象上都定义一个特殊的key,这个key都指向统一的value,即我们上文定义的anyObj。我们要做的就是在查找变量的时候想办法让其能找到这个对象即可。eg:
var anyObj={
toString(){console.log('anyObj toString')},
valueOf(){}
}
function createPerson(name){
var obj={
name:name,
sayName(){console.log(name)}
}
obj.__proto__ = anyObj
return obj
}
var zhangsan = createPerson('zhangsan')
zhangsan.toString()
// 上面的代码可以直接在js环境中运行一下
js的确就是按照我们上面分析的方式处理的,它定义的特殊的key即为 `__proto__`,而且它还处理了`.`操作符,让其在查找对象的时候,如果在对象(假设该对象叫obj)上查不到该属性的话,就去`obj.__propto__`属性上去找,如果obj.__propto__上没有,就去`obj.__proto__.__proto`上去找,直到找到该属性或者__proto__为null,而这个__proto__链其实就是**原型链**。
为了构造原型链,它还引入了构造函数的概念,而构造函数这个概念就包含着new,this,extends,call,bind,apply等概念,我们接下来会一一实现。我们先用构造函数实现一下创建张三的过程。eg:
function createPerson(name){
// var obj={
// name:name,
// sayName(){console.log(name)}
// }
// 用this替代上栗中的obj
this.name = name
this.sayName = function(){console.log(name)}
// obj.__proto__ = anyObj
// proto链并不需要手动构建了,而是通过new关键字去构建,这里具体是如何构建的,我们将在下文讨论
// return obj
// 使用this取代obj以后也不用返回this,new关键字会默认取this去返回。
// 不过这里为了方便特殊情况,this也是不一定返回的,js规定,构造函数如果自己返回了对象,那就不返回this了。
}
var zhangsan = new createPerson('zhangsan')
zhangsan.toString()
构造函数配合new和this两个关键字帮我们将createPerson函数内的代码量直接缩减成了两行 !而且这里其实多了几个概念:
- 构造函数: 我们最终需要的对象的属性和方法的载体,如上文的
createPerson
,我们一般推荐构造函数首字母大写,而且最好是名词,表示一类事物,所以我们可以写为Person
- 实例: 构造函数实例化的对象,即上文中的
zhangsan
,它可以访问到其构造函数上的属性和方法 - new: 将构造函数实例化。
- this: 构造函数中实例的代指。
构造函数中为什么会有prototype?
还是拿上例举例:
function createPerson(name){
this.name = name
this.sayName = function(){console.log(name)}
}
var zhangsan = new createPerson('zhangsan')
var lisi = new createPerson('lisi')
我们在创建zhangsan和lisi的时候都在实例上定义了sayName方法,但其实这两个sayName方法是完全一样的,那就没有必要再在每个实例上都定义一遍这个方法了,类似于之前例子中的anyOb
放到obj.__proto__
一样,把这个sayName放到实例的某一层__proto__上就不用每次都定义实例上啦。js又的确这样干了,它通过new关键字,把构造函数的prototype属性挂到了实例的__proto__上。而prototype(原型)是一个对象,该对象上我们一般只放方法。在prototype上的方法我们一般都称作是原型方法,this上我们一般只放置属性,this上的属性就叫实例属性。prototype上会有一个特殊的方法constructor
,它指向构造函数。注意这里有一个相互引用,即Fn.prototype = prototype; prototype.constructor = Fn
new究竟干了什么?
根据上面的内容,我们已经可以定义一下new究竟对构造函数做了什么了,即实例化构造函数究竟是个啥。
- 创造一个空对象例如:
obj
- 执行构造函数,将构造函数的this绑定成
obj
- 将该构造函数的
prototype
赋值给obj.__proto__
- 如果构造函数返回值不是一个对象,返回
obj
,否则返回构造函数的返回值
// 接下来用代码翻译以下
function _new(Fn,...FnArgs){
var obj = {} // step1
var res = Fn.call(obj,...FnArgs) // step2
obj.__proto__ = Fn.prototype // step3,
/*
注意,其实我们是不推荐显示操作__proto__的,可以使用Object.setPrototypeOf(obj,prototype)
,不过这个方法其实也不太推荐因为会改变所有有相关指向的对象的原型链,比较好的方式是合并1,4,用Object.create方法,即
var obj = Object.create(Fn.prototype)//创建之初就修正好__proto__
*/
return res instanceof Object ? res : obj // step4
}
// 用用看
function Person(name){
this.name = name
}
Person.prototype.sayName = function(){console.log(name)}
var zhangsan = _new(Person,'张三')
原型链具体是如何构建成的呢?
上面讲到new关键字的作用。其中有一个功能就是让实例的__proto__指向构造函数的prototype。而这一步,其实就是构建了一节原型链。那整条原型链是怎样构建的呢?下面是具体的步骤:
- 基于上文我们知道
zhangsan.__proto__ = Person.prototype
- 函数也是对象,所以构造函数Person在有prototype属性的同时,也会有__proto__属性
- 函数的的__proto__也是一个对象,其也是通过构造函数创建出来的,这个构造函数就是 function Object.
- 那么基于实例的__proto__指向构造函数的protype的理论,我们得出:
Person.prototype.__proto__ = Object.prototype
- Object.prototype 也是一个对象,那么它的__proto__指向哪里呢?也指向Object.prototype?那这样的话就形成了相互引用,而且查找起来,原型链永无链头了(这里可以理解成递归去遍历一个数据,条件就是有__proto__属性,这种相互引用,递归永远没有尽头)。
- 不妨回归我们的本意,我们构建原型链其实是为了共享一些属性方法的,既然已经在Object.prototype查找过了,那就没有必要再查找了,而且它也是有数据的最后一层了,它都找完了,那就是所有数据都查过了。如果这个时候还没有,那就该结束原型链的查找了,就像给递归设置一个终止条件一样,我们把链头设置成null。即
Object.prototype**.**__proto__ = null
,这样既能全部查找一遍,也能结束查找了。
// 代码总结一下
zhangsan = new Person
zhangsan.__proto__ === Person.prototype
// Person.prototype是一个对象,也是构造出来的,它的构造函数是function Object
Person.prototype.__proto__ === Object.prototype
// Object.prototype.__proto__ 按照规定,为null
Object.prototype.__proto__ === null
我们上文虽然把zhangsan的原型链查找到了,不过这里还引入了一个新的构造函数Object
,那么Object函数是怎么来的呢,它的原型链是怎么查找的呢?
Object
是一个函数,函数一般都是new Function构造出来的。即Object.__proto__ = Function.prototype
,而Function.protype是一个对象,所以它也是Object构建出来的即Function.prototype.__proto__ = Object.prototype
。而从上一个例子中我们知道了Object.prototype.__proto__ = null
。代码总结一下
Object // = new Function(someArgs) Object其实是一个内置函数
Object.__proto__ === Function.prototype
Function.prototype.__proto__ === Object.prototype
Object.prototype.__proto === null
其实推导Object原型链的时候,我们又引入了一个构造函数Function
,那么Function的原型链是怎样的呢?
Function即是一个函数,又是一个对象。所以Function上既有__proto__又有prototype。Function.prototype的原型链在Object里已经介绍过了,我们重点探讨一下Function.__proto__
。Function是一个函数,那么其__proto__属性就指向其构造函数的protype属性。那么Function的构造函数的谁呢?我们先考虑new个什么能创建一个functio呢?答案其实很明确,new Function 能创建一个function.那么同理,我们也能得出,Function的构造函数式它本身,即Funtion.__proto__ = Function.prototype
,这里其实还有一个问题,Function的构造函数式它自己,那这里不会出现查找无限嵌套死循环的问题吗?答案当然是不会,原因就是这个所谓的查找是原型链查找,它只看实例的__proto__属性,而Function.prototype的__proto__已经在上文中讲过了。至此,原型链基本的构建已经讲完了,不过我们这里还少了一个概念,那就会继承。而继承在class语法糖中是用extends和super关键字实现的。他们究竟干了什么呢?
类中的继承是如何实现的以及它的原型链是怎样的?
先写一个继承类,然后去分析以及实现一下extends,上面的问题应该就解决了。直接上代码:
class Father {
static fn1() { console.log('fn1') }
constructor() {
this.a = 'a'
}
getA() { return this.a }
}
class Child extends Father {
static fn2() { console.log('fn2') }
constructor() {
super()
this.b = 'b'
}
getB() { return this.b }
}
let jim = new Child
先看下结果,类上的实例属性都挂到了最终实例
jim
上,这里其实很好实现,实例在类中一般是用this表示的,我们只要把所有实例属性,不管是Child类还是Father类的,都挂到this上,到时候通过new挂到实例上即可,这个功能是由super实现的。
需要思考一下的,主要是原型链,单纯看原型链关系其实能能看来,这个结构像是让Father成了Child的构造函数。因为jim的__proto__指向了Child的prototype,而jim.propto.proto 指向了Father.prototype,又jim.propto = Child.prototype即Child.prototype.__proto__ = Father.prototype
,这个功能是由extends实现的。
分析完以后,我们实现一下继承的代码。
// 先看一下ts中类转化成构造函数以后,具体是怎么展示的:
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
}
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var Father = /** @class */ (function () {
function Father() {
this.a = 'a';
}
Father.fn1 = function () { console.log('fn1'); };
Father.prototype.getA = function () { return this.a; };
return Father;
}());
var Child = /** @class */ (function (_super) {
__extends(Child, _super);
function Child() {
var _this = _super.call(this) || this;
_this.b = 'b';
return _this;
}
Child.fn2 = function () { console.log('fn2'); };
Child.prototype.getB = function () { return this.b; };
return Child;
}(Father));
var jim = new Child;
分析ts转义的代码可以知道,这里主要是通过_super和__extend来实现继承的。其他和构造函数写法基本一致。从上面代码可知,_super其实就是父构造函数,将父构造函数的实例属性放到子类上,其实执行 _super.call(this)
即可。而原型的继承则是通过__extends来实现的:__extends(Child, _super)
,我们自己实现一个这个方法并分析一下。
// 用法 __extends(Child, _super);
function __extends (child,father){
// 先拷贝静态属性。这里有两种方式,一种是通过for...in和object.hasOwnProperty来取出父元素的静态属性,赋值给子元素。另外一种是,
// 让子元素的__proto__指向父元素,这样原型链可以直接找到父元素,也就能查找父元素上的变量了。
// 这里我们用第一种,因为一般不建议直接使用__proto__
for(let key in father){
if(father.hasOwnProperty(key)){
child[key]=father[key]
}
}
// 这一步就是复制原型了。目的为实现 child.prototype.__propto__ = father.prototype
let obj = Object.create(father.prototype)// obj.__proto__ = father.prototype
child.prototype = obj
child.prototype.constructor = child // 修改一下构造器指向。
}
我们自己实现的extends里没有用new,这是因为这里直接用了new的原理,也直接省去了中间函数的步骤,比较易于理解,并且工能是完全一样的,可以去浏览器或者node环境中debug查看效果。
看几道题(持续更新)
function A(){this.name='a';}
function B(){this.name='b';}
A.prototype.getName = function() {return this.name;}
B.prototype.getName = function() {return this.name;}
A.prototype = new B;
const c = new A;
c.getName();?
/*
先构建原型链
c=new A => c = {name:'a'}
// 因为 A.prototype = new B; 所以忽略 A.prototype.getName = function() {return this.name;}
c.__proto__ = A.prototype = new B = {name:'b'}
// 上面是属性,下面看方法,方法主要是挂载在原型上的,即要看prototype
c.__proto__ = new B
c.__proto__.__proto__ = B.prototype = { getName : function() {return this.name;} }
// 回到基础问题
c.getName() => c.__proto__.__proto__.getName=> function() {return this.name;}
// 这里其实有一个知识点,那就是原型链上的this都是指向实例的
即返回c.name => 'a'
// 如过把 this.name='a' 删除,那么按照原型链的查找,c.name => 'b'
*/
转载自:https://juejin.cn/post/7245297060613324859