js-构造函数、原型、原型链、继承 【干货】

115 阅读2分钟

一、构造函数

1.1 概述:

js语言中使用构造函数(constructor)作为对象的模板。所谓构造函数,就是提供一个生成对象的模板,并描述对象的基本结构的函数。一个构造函数,可以生成多个对象,每个对象都有相同的结构。

基本结构

        function Person(name, age) {
            this.name = name;
            this.age = age;
        }
        var xiaoMing = new Person('小明',20)
        console.log(xiaoMing); // Person {name: '小明', age: 20}

构造函数的三大特点:

  • 构造函数的函数名的第一个字母通常大写。
  • 函数体内使用this关键字,代表所要生成的对象实例。
  • 生成对象的时候,必须使用new命令来调用构造函数。

1.2 new 命令

new命令的作用,就是执行一个构造函数,并且返回一个对象实例。使用new命令时,它后面的函数调用就不是正常的调用,而是依次执行下面的步骤。

  • 创建一个空对象,作为将要返回的对象实例。
  • 将空对象的原型指向了构造函数的prototype属性。
  • 将空对象赋值给构造函数内部的this关键字。
  • 开始执行构造函数内部的代码。

1.3 构造函数有无返回值

  • 如果构造函数内部有return语句,而且return后面跟着一个复杂数据类型(对象,数组等),new命令会返回return语句指定的对象;
  • 如果return语句后面跟着一个简单数据类型(字符串,布尔值,数字等),则会忽略return语句,返回this对象。
        function Person1() {
            this.age = 22;
            return {
                age: 18
            };

        }
        var p1 = new Person1();
        console.log(p1.age); // 18

        function Person2() {
            this.age = 22;
            return 18;
        }
        var p2 = new Person2();
        console.log(p2.age); // 22

1.4 防止不使用new调用构造函数

        function Person(name, age) {
            if (!(this instanceof Person)) return new Person(name, age);
            this.name = name;
            this.age = age;
        }
        var p = Person('张三', 22);
        console.log(p);

1.5 手写new

     function selfNew(ctor, ...args) {
            // 创建一个空对象,该对象的原型指向购找函数的原型对象
            var obj = Object.create(ctor.prototype)
            // 调用构造函数
            var result = ctor.apply(obj, args)
            // 如果构造函数有返回值,并且返回值是一个对象或方法,则返回该对象,否则返回新生成的对象
            return typeof result == 'object' || typeof result == 'function' ? result : obj
        }

        // 测试代码
        function Person(name, age) {
            this.name = name;
            this.age = age
        }
        Person.prototype.sing = function () {
            return `我叫${this.name},我喜欢唱歌!`
        }
        var fzw = selfNew(Person, '范志伟', 26)
        console.log(fzw);
        console.log(fzw.sing());

二、构造函数、原型对象和实例之间的关系

2.1 prototype

所有函数都会自动创建一个 prototype (显式原型)属性,属性值也是一个普通的对象。对象以其原型为模板,从原型继承方法和属性,这些属性和方法定义在对象的构造器函数的 prototype 属性上,而非对象实例本身。

但有一个例外: Function.prototype.bind(),它并没有 prototype 属性

t2.image

如图所示 Foo 对象有一个原型对象 Foo.prototype,其上有两个属性,分别是 constructor 和 __proto__,其中 __proto__ 已被弃用。

构造函数 Foo 有一个指向原型的指针,原型 Foo.prototype 有一个指向构造函数的指针 Foo.prototype.constructor,这就是一个循环引用,即:

Foo.prototype.constructor === Foo; // true
t3.image

2.2 __proto__

每个实例对象(object )都有一个隐式原型属性(称之为 __proto__ )指向了创建该对象的构造函数的原型。也就是指向了函数的 prototype 属性。

function Foo () {}
let foo = new Foo()
t4.image

当 new Foo() 时,__proto__ 被自动创建。并且

foo.__proto__ === Foo.prototype; // true
t5.image

2.3 原型链

每个对象拥有一个原型对象,通过 __proto__ 指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null,这种关系被称为原型链(prototype chain)。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

t1.awebp
function F(){}
var f = new F();
// 构造器
F.prototype.constructor === F; // true
F.__proto__ === Function.prototype; // true
Function.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true

// 实例
f.__proto__ === F.prototype; // true
F.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true

三、继承

3.1 原型链继承

  • 通过子构造函数原型对象直接指向父类实例

实现:

        function Parent() {
            this.name = '皇帝';
            this.hobbies = ['抽烟', '喝酒']
        }
        Parent.prototype.doSth = function () {
            console.log(`我是${this.name},爱好${this.hobbies}`);
        }
        function Child(n) {
            this.name = '太子' + n;
        }
        // 继承了Parent
        Child.prototype = new Parent();
        Child.prototype.constructor = Child
        
        var child1 = new Child(1);
        child1.hobbies.push('烫头')
        var child2 = new Child(2);
        child1.hobbies.push('火锅')
        child1.doSth()
        child2.doSth()
        
        // 我是太子1,爱好抽烟,喝酒,烫头,火锅
        // 我是太子2, 爱好抽烟, 喝酒, 烫头, 火锅

优点:

  1. 父类可以复用

缺点:

  1. 引用类型值的原型属性会被所有实例共享
  2. 不能给父类传递参数

3.2 借用构造函数继承

子类构造函数内部通过 父类.call() 直接指向子类

实现:

        function Parent(name = '皇帝', hobbies = ['抽烟', '喝酒']) {
            this.name = name;
            this.hobbies = hobbies;
        }
        Parent.prototype.doSth = function () {
            console.log(`我是${this.name},爱好${this.hobbies}`);
        }
        function Child(name, hobbies) {
            // 继承了Parent
            Parent.call(this, name, hobbies);
        }

        var child1 = new Child('太子1', hobbies = ['抽烟', '喝酒', '烫头']);
        console.log(child1);
        
        var child2 = new Child('太子2');
        child2.hobbies.push('网吧')
        console.log(child2);

        console.log('child1.doSth:', child1.doSth()); // 报错

优点:

  1. 因为是每次都调用了父类,所以不会子类生成的实例不会共享同一个实例。
  2. 可以给父类传参

缺点:

  1. 仅仅是借用了构造函数,方法只能在构造函数中定义,每次调用子类构造函数都会调用父类构造函数。
  2. 父类原型上定义的方法,对于子类无法找到。

3.3 组合继承

  • 原型继承+借用构造函数继承

实现:

        function Parent(name = '皇帝', hobbies = ['抽烟', '喝酒']) {
            this.name = name;
            this.hobbies = hobbies;
        }
        Parent.prototype.doSth = function () {
            console.log(`我是${this.name},爱好${this.hobbies}`);
        }

        function Child(name, hobbies) {
            //继承了Parent  关键代码 
            Parent.call(this, name, hobbies);
        }

        // 原型对象指向父类实例 继承 关键代码
        Child.prototype = new Parent()
        // 子类原型对象指向子类 形成环引用
        Child.prototype.constructor = Child;

        var child1 = new Child('太子1', hobbies = ['抽烟', '喝酒', '烫头']);
        child1.doSth()
        var child2 = new Child('太子2');
        child2.hobbies.push('网吧')
        child2.doSth()

优点:

  1. 融合原型链继承和借用构造函数的优点

缺点:

  1. 父类调用两次,一次是用new,一次是用call

3.4 原型式继承

实现:

        const person = {
            name: "刘邦",
            friends: ["张良", "韩信", "萧何"],
            jieShao() {
                console.log(`我是${name},我的朋友是${friends}`);
            }
        };
        // es5之前
        function object(o) {
            function F() { }
            F.prototype = o;
            return new F();
        }
        const p1 = object(person)
        console.log(p1);
        console.log(Object.getPrototypeOf(p1));
        
        // es5 之后 Object.create(要继承的对象)
        const p2 = Object.create(person)
        p2.friends.push('樊哙')
        console.log(Object.getPrototypeOf(p1));
        console.log(Object.getPrototypeOf(p2));

缺点:

  1. 引用类型值的属性始终都会共享相应的值,就像使用原型模式一样

3.5 寄生式继承

原型式继承+工厂函数

实现

        // 工厂函数创建对象
        function createObj(o) {
            // 原型式克隆
            var clone = Object.create(o); //创建一个新对象
            clone.sayHi = function () { //以某种方式来增强这个对象
                console.log("hi,大家好");
            };
            return clone; //返回这个对象
        }

        const person = {
            name: "刘邦",
            friends: ["张良", "韩信", "萧何"],
            jieShao() {
                console.log(`我是${name},我的朋友是${friends}`);
            }
        };
        var p1 = createObj(person)
        console.log(p1);
        console.log(Object.getPrototypeOf(p1));

        var p2 = createObj(person)
        p2.friends.push('樊哙')
        console.log(Object.getPrototypeOf(p1));
        console.log(Object.getPrototypeOf(p2));

优点:

  1. 根据一个对象克隆创建另一个对象,并增强对象

缺点:

  1. 引用类型值的属性始终都会共享相应的值
  2. 跟借用构造函数模式一样,每次创建对象都会创建一遍方法。

3.6 寄生组合式继承

组合继承+原型式

实现

        function Parent(name) {
            this.name = name;
            this.colors = ["red", "blue", "green"];
        }
        Parent.prototype.sayName = function () {
            console.log(this.name);
        };
        function Child(name, age) {
            // 组合式关键代码
            Parent.call(this, name);
            this.age = age;
        }

        // 组合式关键代码 ;  Object.create(Parent.prototype) 原型式继承
        Child.prototype = Object.create(Parent.prototype);
        Child.prototype.constructor = Child;
        Child.__proto__ = Parent;
        
        Child.prototype.sayAge = function () {
            console.log(this.age);
        };
        var child1 = new Child('太子1', 18);
        var child2 = new Child('太子2', 22);
        child2.colors.push('black')
        console.log(child1);
        console.log(child2);