构造函数--继承

本文深入探讨了构造函数的工作原理,包括this关键字的行为、构造函数的返回值处理、实例属性与原型属性的区别,以及多种继承模式如类式、构造函数、组合、原型式、寄生式和寄生组合式的实现与优缺点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

构造函数的理解

构造函数在通过new关键字调用的过程中会创建一个对象,并作为参数传递给构造函数。

如果构造函数返回值是 一般的数据类型, 构造函数就会忽略这个值,实例不会改变 

如果构造函数的返回值是引用数据类型,构造函数就会忽略this,实例就会变成构造函数返回的对象

var button = {
  clicked: false,
  click: () => {
    console.log(this, 'this')
  }
}
button.click() // 非严格模式下是window 严格模式下是空对象{}

var button2 = {
  clicked: false,
  click: function (){
    console.log(this, 'this')
  }
}
button2.click() // 指向button2
function Button() {
  this.isClick = false,
  this.click = function() {
    console.log(this)
  }
}

const button = new Button()
button.click() // Button {isClick: false, click: ƒ}
console.log(button) // Button {isClick: false, click: ƒ}
document.getElementById('mydiv').addEventListener('click', button.click) // 一个html元素

换成箭头函数来解决 

function Button() {
  this.isClick = false,
  this.click = () => {
    console.log(this)
  }
}

const button = new Button()
button.click() // Button {isClick: false, click: ƒ}
document.getElementById('mydiv').addEventListener('click', button.click) // Button {isClick: false, click: ƒ}

总结函数中this

  • 作为普通函数调用,非严格模式下是window, 严格模式下是undifiend
  • 作为对象方法来调用,指向对象本身,如果对象方法是箭头函数,指向对象所在的作用域内的this。
  • 作为构造函数来调用,指向新创建的对象,即实例。
  • 通过call,apply来调用,this指向call,apply的第一个参数
  • 箭头函数没有单独的this,this在箭头函数创建时确定,始终指向创建箭头函数作用域内部的this。有全局的吗,有局部的。
  • 所有函数都可以使用bind方法来改变this, 箭头函数除外,箭头函数的this无法改变。

构造函数的原型属性和实例属性

function Foo() {
  this.prop = 123;
}

Foo.prototype.prop2 = 555;



var f1 = new Foo()
    f2 = new Foo();

console.log(f1.prop, f2.prop); // 123 123
f1.prop = 444;
console.log(f1.prop, f2.prop); // 444 123

console.log(f1.prop2, f2.prop2); // 555 555
f1.__proto__.prop2 = 666;
console.log(f1.prop2, f2.prop2); // 666 666
f1.prop2 = 777;
console.log(f1.prop2, f2.prop2); // 777 666

思考一下最后一个打印为什么是777 666不是两个都是777?因为f1访问的自己的属性不是原型上的f2访问的原型上的属性。

如果共享属性也是对象会怎样呢?

 

Foo.prototype.prop3 = {
    name: '麦乐'
}

f1.prop3.name = '改变'

console.log(f1.prop3) // {name: "改变"}

console.log(f2.prop3) // {name: "改变"}

两个的取值都改变了,因为这个时候两个实例取的都是原型上的值。如果像下面这样改动一下呢?

f1.prop3 = {
    name: '设计'
}
console.log(f1.prop3)  // {name: "设计"}


console.log(f2.prop3) // {name: "改变"}

f1.__proto__.prop3 = {
    name: '奇怪吗?'
}

console.log(f1.prop3)  // // {name: "设计"}


console.log(f2.prop3) // {name: "奇怪吗?"}

这里看懂了吗? 

f1.prop3 = {}这种方式改变属性,都是改变的自己的,不是原型上面的,所以结果会有所不同。

如果你了解原理,怎么变都不奇怪。 

构造函数的constructor

我们说的constructor一般指类的prototype.constructorprototype.constructor是prototype上的一个保留属性,这个属性就指向类函数本身,用于指示当前类的构造函数。

那constructor能不能被修改呢?

function Person(age) {
    this.age = age;
}
console.log(Person.prototype.constructor === Person); // true

Person.prototype.constructor = function NewPerson(age) {
    this.age = age + 1;
}
var person = new Person(18);
console.log(person.age) // 18

上例说明,我们修改prototype.constructor只是修改了这个指针而已,并没有修改真正的构造函数。 

再来看一个有趣的现象

function Person(age) {
    this.age = age;
}

console.dir(Person.prototype.constructor);
console.dir(Person.constructor);

两个结果完全不一样,这是因为Person.prototype.constructor指向Person;访问Person.constructor会先在Person上找这个属性,发现Person上没有,就去原型上找,Person的原型是什么呢?展开看一下,Person.__proto__ ,这个对象指向谁呢?在这里Person也是一个实例,是Function的实例,因为任何一个函数都是Function的实例,所以Person.__proto__指向Function.prototype。自然就找到了Function.prototype上面的构造函数,而这个构造函数又指向Function,也就是Function。

自己实现一个new

function myNew(fun, ...args) {
    const instance = {};
    const result = fun.call(instance, ...args);
    // 如果没有这一步,也能生成一个新的对象,但是原型却不是指向fun的
    instance.__proto__ = fun.prototype; // 设置原型
    // 注意如果原构造函数有Object类型的返回值,包括Functoin, Array, Date, RegExg, Error
    // 那么应该返回这个返回值
    const isObject = typeof result === 'object' && result !== null;
    const isFunction = typeof result === 'function';
    if (isObject || isFunction) {
        return result;
    }
    return instance;
}

构造函数的继承

面向对象怎么能没有继承呢,根据前面所讲的知识,我们其实已经能够自己写一个继承了。所谓继承不就是子类能够继承父类的属性和方法吗?换句话说就是子类能够找到父类的prototype,最简单的方法就是子类原型的__proto__指向父类原型就行了。

function Parent() {}
function Child() {}

Child.prototype.__proto__ = Parent.prototype;

const obj = new Child();
console.log(obj instanceof Child );   // true
console.log(obj instanceof Parent );   // true

上面的写法有没有觉得有点熟悉

 根据原型,原型链,如果没有上面的赋值继承,下面的写法是不是也是对的?

console.log(Child.prototype.__proto__ === Object.prototype) // true

上述继承方法就是让Child原型属性的原型指向Parent.prototype而不是Object.prototype,这样根据原型链原理,Child的实例就可以去访问Parent.prototype上的属性和方法,但是这里并没有没有执行Parent的构造函数:

function Parent() {
  this.parentAge = 50;
}
function Child() {}

Child.prototype.__proto__ = Parent.prototype;

const obj = new Child();
console.log(obj.parentAge);    // undefined

Child的实例访问不了Parent自己的属性和方法,只能访问原型上面的。

为了解决这个问题,我们不能单纯的修改Child.prototype.__proto__指向,还需要用new执行下Parent的构造函数:

1 子类的原型对象-类式继承

  // 声明父类
    function superClass() {
      this.superValue = ['设计模式1', '你所不知道的JavaScript']
    }
    superClass.prototype.getSuperValue = function() {
      console.log(this.superValue)
    }
    // 声明子类
    function subClass() {
      this.superValue = ['设计模式2', '你所不知道的JavaScript']
    }
    subClass.prototype = new superClass()
    subClass.prototype.getSubValue = function() {
      console.log(this.superValue)
    }

类式继承就是将第一个类的实例赋值给子类的原型。

var instance = new subClass()
instance.getSuperValue() //(2) ["设计模式2", "你所不知道的JavaScript"]
instance.getSubValue() //(2) ["设计模式2", "你所不知道的JavaScript"]

子类可以访问父类的属性和方法。

这类继承的本质就是,子类的实例也是父类的实例。

    var instance1 = new subClass()
   
    console.log(instance1 instanceof superClass) // true
    console.log(instance1 instanceof subClass) // true

但是又跟父类的实例不同,下面是两个父类实例,两者互不影响:

// 声明父类
    function superClass() {
      this.superValue = ['设计模式1', '你所不知道的JavaScript']
    }
    superClass.prototype.getSuperValue = function() {
      console.log(this.superValue)
    }
    var superInstance1 = new superClass()
    var superInstance2 = new superClass()
    superInstance1.superValue.push('隐者秘籍')
    console.log(superInstance1.superValue) // ["设计模式1", "你所不知道的JavaScript", "隐者秘籍"]
    console.log(superInstance2.superValue) // ["设计模式1", "你所不知道的JavaScript"]

再看子类。 

这里调整一下父类的属性值。

// 声明父类
    function superClass() {
      this.superValue = 2
    }
var instance1 = new subClass()
var instance2 = new subClass()

 子类实例1的属性改变,没有对子类实例2产生影响。好像没有什么不同。

instance1.superValue = 3
console.log(instance1.superValue) // 3
console.log(instance2.superValue) // 2

再看另外一个情况,父类属性是引用类型。 

 // 声明父类
    function superClass() {
      this.superValue = ['设计模式1', '你所不知道的JavaScript']
    }

子类实例1的改变,影响到了子类实例2。也就是说,如果父类的属性是引用类型,就会相互影响。这也是类式继承的缺点所在。

    var instance1 = new subClass()
    var instance2 = new subClass()
    instance1.superValue.push('隐者秘笈') 
    console.log(instance1.superValue) // (3) ["设计模式1", "你所不知道的JavaScript", "隐者秘笈"]
    console.log(instance2.superValue) // (3) ["设计模式1", "你所不知道的JavaScript", "隐者秘笈"]

下面是构造函数原型属性的特性,原型对象上面的属性值如果是引用类型,所有实例会共用,值如果是非引用类型,相互不影响。


function superClass(id) {
}
superClass.prototype = {
    list: ['设计模式','肖申克的救赎'],
    id: 10,
}
superClass.id= 10
const instance1 = new superClass();
const instance2 = new superClass();
instance1.list.push('原型链')
console.log(instance1.list) // ["设计模式", "肖申克的救赎", "原型链"]
console.log(instance2.list) // ["设计模式", "肖申克的救赎", "原型链"]
instance1.id = 11
console.log(instance1.id) // 11
console.log(instance2.id) // 10

因为通过 instance1.id = 11赋值的过程,改变的是instance1对象自身的属性值,不是原型上面的。instance1.list.push('原型链'),处于引用类型指向同一个地址,所有改变的是原型上面的属性。如果通过这样的方式赋值 instance1.list = [...],将不会修改原型,不会影响其它实例。

2 构造函数继承

这种方法是在子类的构造函数中调用父类的构造函数,优点是解决了上面引用类型相互影响的问题,缺点是无法继承父类原型上的方法。

// 声明父类
function superClass(id) {
    this.list = ['设计模式','肖申克的救赎']
    this.id = 10
}
superClass.prototype.sayList = function(){
    console.log(this.list)
}

// 声明子类
function subClass(id) {
    superClass.call(this, id)
}
subClass.prototype.sayId = function() {
    console.log(this.id)
}

const instance1 = new subClass(11)
const instance2 = new subClass(12)
instance1.list.push('原型链');
console.log(instance1.list) // ["设计模式", "肖申克的救赎", "原型链"]
console.log(instance2.list) // ["设计模式", "肖申克的救赎"]

3 组合继承

结合以上两者继承,子类中调用父类的构造函数,子类的原型等于父类的实例,这样既可以访问父类的属性 ,也可以访问父类的方法。又不会相互影响。

// 声明父类
function superClass(id) {
    this.list = ['设计模式','肖申克的救赎']
    this.id = id
}
superClass.prototype.sayList = function(){
    console.log(this.list)
}

// 声明子类
function subClass(id) {
    superClass.call(this, id)
}
subClass.prototype = new superClass()
subClass.prototype.sayId = function() {
    console.log(this.id)
}

const instance1 = new subClass(11)
const instance2 = new subClass(12)
instance1.list.push('原型链');
console.log(instance1.list) // ["设计模式", "肖申克的救赎", "原型链"]
console.log(instance2.list) // ["设计模式", "肖申克的救赎"]
instance1.sayList() // ["设计模式", "肖申克的救赎", "原型链"]
instance2.sayList() // ["设计模式", "肖申克的救赎"]

但这种方式还不是最完美的,因为在子类中调用了两次父类的构造函数。superClass.call()调用了一次,new superClass()又重复调用了一次。

4 原型式继承

跟类式继承有点类似,但又有所不同,中间用一个纯净的构造函数来过渡。不需要实例化父类的构造函数。类式继承子类的原型指向父类的实例,父类实例的原型指向父类。原型继承子类的原型指向父类实例,父类实例的原型不再指向父类,而是中间过渡的纯净的函数F。但同样会存在引用类型共用的问题

function initObj(o) {
  function F() {}
  F.prototype = o
  return new F();
}
const book = {
  list: ['js', 'css'],
  year: 1988
}
const book1 = initObj(book)
const book2 = initObj(book)

console.log(book1) // F {}
console.log(book2) // F {}

book1.list.push('java')
console.log(book1.list) // ["js", "css", "java"]
console.log(book2.list) // ["js", "css", "java"]

5 寄生式继承

    function initObj(o) {
        function F() {}
        F.prototype = o
        return new F();
    }
    const book = {
        list: ['js', 'css'],
        year: 1988
    }
    function createBook(obj) {
        var o = initObj(obj);
        o.sayName = function() {
            console.log(2)
        }
    }

寄生继承首先将一个普通对象book克隆进一个叫o的对象,然后扩展o对象,添加更多的属性,最后返回o对象。这种寄生继承就是对原型继承的第二次封装,并且在这第二次封装过程中对继承的对象进行了拓展,这项新创建的对象不仅仅有父类中的属性和方法而且还有新添加的属性和方法。

6 寄生组合式继承

引用类型互不影响,父类的构造函数只执行一次。完美的继承方式。

先用构造函数式继承,继承父类的属性和方法,又通过寄生式继承,继承了父类的原型。

   function initObj(o) {
        function F() {}
        F.prototype = o
        return new F();
    }
  
    function inheritPrototype(SubClass, SuperClass) {
        var o = initObj(SuperClass.prototype);
       o.constructor = SubClass;
       SubClass.prototype = o;
    }

    // 声明父类
    function SuperClass(id) {
        this.list = ['设计模式','肖申克的救赎']
        this.id = id
    }
    SuperClass.prototype.sayList = function(){
        console.log(this.list)
    }

    // 声明子类
    function SubClass(id) {
        SuperClass.call(this, id)
        this.style = 'black'
    }
    inheritPrototype(SubClass, SuperClass)
    SubClass.prototype.sayId = function() {
        console.log(this.id)
    }

    var instance1 = new SubClass(1)
    var instance2 = new SubClass(2)
    instance1.sayId() // 1
    instance2.sayId() // 2
    instance1.list.push('java')
    instance1.sayList() //  ["设计模式", "肖申克的救赎", "java"]
    instance2.sayList() //  ["设计模式", "肖申克的救赎"]

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值