从构造函数联想出的点点思考

138 阅读5分钟

起因

写这篇文章的起因在于一次面试(面试官真的太温柔了),面试官问我构造函数运行的时候发生了什么?当时我就愣住了,难道构造函数运行的时候不是运行了构造函数吗?顺着这个问题我想了一会,感觉以前看到的很多东西都串联起来了

构造函数

构造函数和普通的函数没有区别,它就是一个函数。我经常能看到这句话,也记住了这句话,但其实并没有理解这句话。

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的引用。

QQ截图20220512104331.png [[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);

方法二

QQ截图20220512110627.png

方法三

QQ截图20220512110559.png

好了,对于方法三我们再深入一点点。我们都知道函数两个人共用一个引用是没有什么问题的,但是如果是个变量呢?例如b.prototype.zx = 3,如果此时我修改x.zx=5y.zx会不会改变呢?答案是不会。

我们直接来看执行结果

QQ截图20220512111238.png

可以看到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就行了嘛。