起因
写这篇文章的起因在于一次面试(面试官真的太温柔了),面试官问我构造函数运行的时候发生了什么?当时我就愣住了,难道构造函数运行的时候不是运行了构造函数吗?顺着这个问题我想了一会,感觉以前看到的很多东西都串联起来了
构造函数
构造函数和普通的函数没有区别,它就是一个函数。我经常能看到这句话,也记住了这句话,但其实并没有理解这句话。
function a(){
let c = 3
}
function b(){
this.c=3
}
上面两个都是函数,但是第二个函数可以用作构造函数。why?其实非常简单,只需要知道this是干啥的,以及作用域是啥就行。
首先先思考,当执行a(),b()的时候会发生什么。
-
a会创建一个函数作用域,然后创建变量c赋值为3,之后函数执行完成,作用域和变量c都销毁。
-
b会创建一个函数作用域,然后把3赋给this上的变量c,this是谁?谁调用就是谁。谁调用?window调用。之后销毁函数作用域。
好了,这俩执行完之后的区别是什么?a执行完之后什么都没有留下,而b给window添加了变量c值为3。
下一个问题,那我就想要a函数里的c怎么办呢,很简单,闭包就可以。
闭包
什么是闭包?其实看完上面之后也就很清楚了,能访问外部作用域就是闭包,或者说当前作用域里有父级作用域的引用就是闭包。
var c = 1
function d(){
console.log(c);
}
d()
function a(){
let c = 3
return ()=>{
console.log(c);
}
}
其中函数d是闭包,return ()=>{console.log(c);}也是闭包。他们都访问到了外部作用域的c,或者说留有外部作用域c的引用。
[[Scopes]]就是作用域链的实体。
那其实这里也就自然而然的引出另一个问题,变量c被其他作用域引用了,a函数执行结束不就不能销毁它了吗,没错所以这样也就形成了内存泄露问题。
new-实例-原型-构造函数-原型链
好了让我们继续回到构造函数的问题上来。上面说到,直接执行b()是给window上添加了变量,那怎么才能用这个东西构造出一个实例对象呢。这就要用到new了。
new
let x = new b(),new做了什么?很简单,new创建了新对象,这个对象的原型指向了函数b的原型(即继承原型链),然后以这个对象的名义去执行函数b。写出来就像这样
function create(fn, ...args) {
if(typeof fn !== 'function') {
throw 'fn must be a function';
}
// 方法一
// var obj = new Object()
// obj.__proto__ = fn.prototype
// 方法二
var obj = Object.create(fn.prototype);
fn.apply(obj, args);
return obj
};
现在问题就简单了,经过这样一番执行,x上面就有了变量c,一个对象就这样被我们创建完成了。
原型
好了都到这一步了,稍微再往下想一想。我想给b添加一个方法fn,有这样三种写法,他们之间有什么区别呢。
function b(){
this.c=3
//方法一
function fn(){
console.log('log');
}
//方法二
this.fn = function(){
console.log('log');
}
}
//方法三
b.prototype.fn = function(){
console.log('log');
}
我相信如果你看懂了前面部分,应该立刻会反应过来,第一种方法根本就是不对的,它只是在b的函数作用域里创建了一个函数fn而已,外部是没有办法使用和保存的。
那二和三呢?一个创建在构造函数里,一个创建在原型上嘛。具体点呢?简单来说,用方法二创建的实例,每一个实例上面都有一个单独的fn,而用方法三所以的实例都共用一个fn。
let x = new b()
let y = new b()
console.log(x);
console.log(y);
方法二
方法三
好了,对于方法三我们再深入一点点。我们都知道函数两个人共用一个引用是没有什么问题的,但是如果是个变量呢?例如b.prototype.zx = 3,如果此时我修改x.zx=5,y.zx会不会改变呢?答案是不会。
我们直接来看执行结果
可以看到x.zx=5,并没有修改原型,只是又创建了一个变量zx给实例x,这样在顺着原型链查找的时候就会先找到x.zx而不是原型上的zx。
那么如果我们就是想修改原型上的怎么办?x.__proto__.zx=5或者x.constructor.prototype=5即可。这里还有一个原型链指向的小知识,简单来说就是x.__proto__.zx === x.constructor.prototype
继承
其实看懂了上面的部分,继承只不过是just so so。原型链继承和构造函数继承就不说了,直接看组合继承和寄生组合继承。
组合继承
function Parent(value) {
this.val = value
}
Parent.prototype.getValue = function() {
console.log(this.val)
}
function Child(value) {
Parent.call(this, value)
}
Child.prototype = new Parent()
const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
寄生组合继承
function Parent(value) {
this.val = value
}
Parent.prototype.getValue = function() {
console.log(this.val)
}
function Child(value) {
Parent.call(this, value)
}
Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child,
enumerable: false,
writable: true,
configurable: true
}
})
const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
继承无非就是继承你的属性,然后继承你的原型链嘛。属性怎么继承?调用你的构造函数啊。原型链怎么继承?我用Parent的原型创建Child的原型不就行了。这样构造函数变成Parent了?把Child的构造函数指向Child就行了嘛。