构造函数原型继承原来这么简单?来吧!深入浅出

Posted Geometric coolness

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了构造函数原型继承原来这么简单?来吧!深入浅出相关的知识,希望对你有一定的参考价值。

构造函数

小编上篇博客中介绍到的通过关键字class方式定义类,然后根据类再创建对象的方式,是ES6中语法,现在很多浏览器对ES6的支持还不是很好,所以也要学习通过构造函数(构建函数)的方式创建对象

问?既然浏览器对ES6的支持不是很好,是不是编写代码时不要使用ES6语法呢?(看完这篇文章你就有答案了)

1.构造函数和原型

1.1对象的三种创建方式–复习

  1. 字面量方式

    var obj = {};
    
  2. new关键字

    var obj = new Object();
    
  3. 构造函数方式

    function Person(name,age){
      this.name = name;
      this.age = age;
    }
    var obj = new Person('zs',12);
    

说明:

构造函数是一种特殊的函数,主要用来初始化对象,即为对象成员的值进行初始化,其与new一起使用用于创建对象

这里所说的构造函数与前面讲的类中的构造函数不太一样,类中的构造函数主要是给类中的属性赋值,这里的构造函数中,既可以包属性,也可以包含方法,与上面所说的类的概念更接近,可以看成是J S 彻底向面向对象转变过程中的一种过度,四不像

所以在编写构造函数时,可以参考类的定义方式,将对象的一些公共的属性和方法抽象出来,然后封装到这个函数中

如下面分别使用ES6中的class和ES5中的构造函数的方式生成实例对象

		/*
        *ES6 中创建对象的方法:先使用class关键字创建类,然后使用 new 类名() 的方式创建类的对象
        */
        class Person {
            constructor(name, age) {
                this.name = name
                this.age = age
            }
            speak(){
                console.log('哇哇哇哇哇哇哇')
            }
        }
        // 创建类的对象
        var p1 = new Person('李白', 20)
        // 访问类中的属性
        console.log(p1.name)
        p1.speak()
        console.log('---------------------')

        /*
        *ES5 中创建对象的方法:先创建构造函数,然后使用 new 函数名称() 的方式创建这个函数的对象
        */
        function Star(name, gender) {
            this.name = name
            this.gender = gender
            this.sing=function(){
                console.log('小呀嘛小二郎')
            }
        }
        // 通过构造函数创建对象
        var s1 = new Star('杜甫', '男')
        console.log(s1.gender)
        s1.sing()

图解

注意两点

1)构造函数首字母大写,就给使用class创建类一样

2)与new关键字一起使用,这更与class一样

3)最为重要的一点是:类的编写方法更像是语法糖,目的是让我们能够快速、舒服、优雅的编写类,但是从本质上来说,类其实就是函数。初学者可能感觉不到 class 方式定义类相较于 构造函数的优势:1)没有体会到通过构造函数的方式实现继承的痛苦,所以无法理解通过 ES6 中 extends 实现继承的优势; 2)没有见识过传统的面向变成语言

console.log(typeof Person);
console.log(typeof FPerson);

结果

new的解释

在内存中创建一个新的空对象

让this指向这个对象,所以在代码中使用this,就是使用这个对象,this 跟类没有关系

执行构造函数中的代码,给这个对象添加属性和方法,但是方法中的代码不会执行

1.2静态成员和实例成员

1.2.1实例成员

实例成员就是构造函数内部通过this添加的成员 如下列代码中 name age sing 就是实例成员,实例成员只能通过实例化的对象来访问

  /*通过this添加的成员就是实例成员
        1)实例成员只能通过对象.成员的方式访问
        2)实例成员与每个对象相关,也就是每个对象的成员的值是不一样的
        */
        function Star(name, gender) {
            this.name = name
            this.gender = gender
            this.sing = function () {
                console.log('小呀嘛小二郎')
            }
        }
        // 通过构造函数创建对象
        var s1 = new Star('杜甫', '男')
        var s2 = new Star('蔡徐坤', '女')
        console.log(s1.gender)
        console.log(s2.gender)
        // 实例成员不能通过 【构造函数名称.成员】 的方式访问
        // console.log(Star.gender) // undefined

这点与ES6中 class 创建的类是一样的

class Star {
    constructor(uname, age) {
        this.uname = uname
        this.age = age
	}
    sing() {
    	console.log('我会唱歌')
    }
}

var ldh=new Star('刘德华',17)
console.log(ldh.uname)

1.2.2静态成员

静态成员 在构造函数本身上添加的成员 如下列代码中 就是静态成员,静态成员只能通过构造函数来访问

这与通过 class 关键字定义类时一样的

总结:

实例成员属于对象,所以两个对象的实例成员的值不一样

静态成员,属于构造函数本身,每个对象都属于这个构造函数,所以多个对象共享一个静态成员

1.3构造函数的问题

构造函数方法很好用,但是存在浪费内存的问题。

1.4构造函数原型prototype

构造函数通过原型分配的函数是所有对象所共享的。

javascript 规定,每一个构造函数都有一个prototype 属性,指向另一个对象。注意这个prototype就是一个对象,叫做原型对象

ES6 中的类也是一样,因为我们说过,类从本质上来讲,也是一个函数

        function Star(name, gender) {
            this.name = name
            this.gender = gender
            this.sing = function () {
                console.log('小呀嘛小二郎')
            }
        }
        console.log(Star.prototype)

这个对象的所有属性和方法,都会被构造函数所拥有

function Star(name, gender) {
            this.name = name
            this.gender = gender
            this.sing = function () {
                console.log('小呀嘛小二郎')
            }
        }
        // 为Star的原型对象添加方法(相当于将方法添加到Star的父类上去)
        Star.prototype.cry=function(){
            console.log('我要cry,cry,cry,cry,cry')
        }
        Star.prototype.dance=function(){
            console.log('一步一步,似魔鬼的步伐')
        }
        // 通过输出发现,原型对象上确实有了 cry 方法
        console.log(Star.prototype)
        // 那么作为原型对象的子类的 Star 构造函数自然就拥有了cry 方法
        var s1 = new Star('杜甫', '男')
        s1.cry()
        s1.dance()
        var s2 = new Star('蔡徐坤', '女')
        s2.cry()
        s2.dance()

还可以使用对象的方式为原型添加多个方法

我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。

解惑:

1)这个原型,就类似于其他语言中的基类。。。。

2)不仅使我们自己使用构造函数或类定义的对象,JS中的内置对象的方法,其实都定义在这个对象的原型对象上

var arr=[]
// 查看对象的原型对象使用 __proto__ 属性
console.log(arr.__proto__)
// 查看构造函数或类的原型对象使用 prototype 属性
console.log(Array.prototype)
// 总结:对象的__proto__ 属性和 类或者构造函数的 protptype 属性指向的是同一个对象

1.5对象原型

构造函数的prototype 属性获取的是当前构造函数的原型对象
构造函数的实例的__proto__属性获取的是当前对象的对象原型
这两者是一个对象,也就是说构造函数的原型对象与此构造函数的实例的对象原型是一个对象

 function Star(name, gender) {
            this.name = name
            this.gender = gender
            this.sing = function () {
                console.log('小呀嘛小二郎')
            }
        }
        // 为Star的原型对象添加方法(相当于将方法添加到Star的父类上去)
        Star.prototype.cry = function () {
            console.log('我要cry,cry,cry,cry,cry')
        }
        Star.prototype.dance = function () {
            console.log('一步一步,似魔鬼的步伐')
        }
        // 通过输出发现,原型对象上确实有了 cry 方法
        console.log(Star.prototype)
        // 那么作为原型对象的子类的 Star 构造函数自然就拥有了cry 方法
        var s1 = new Star('杜甫', '男')
        // s1.cry()
        // s1对象的 __proto__属性获取是的是s1对象的对象原型
        console.log(s1.__proto__)
        // 验证构造函数的原型对象与实力的对象原型是一个对象
        console.log(Star.prototype===s1.__proto__)

1.6constructor构造函数

对象原型( proto)和构造函数(prototype)原型对象里面都有一个属性 constructor 属性(因为两个其实是一个东西) ,constructor 我们称为构造函数,因为它指回构造函数本身。

下面通过代码理解

        function Star(name, gender) {
            this.name = name
            this.gender = gender
            this.sing = function () {
                console.log('小呀嘛小二郎')
            }
        }      
        var s1 = new Star('杜甫', '男')
        // 输出构造函数的原型对象
        console.log(Star.prototype)
        // 输出对象的对象原型
        console.log(s1.__proto__)
        

通过上面的代码,我们看到,constructor 属性的值确实是这个对象对应的构造函数

不仅可以通过这个属性,获取原型对象所属的构造函数,constructor属性 还可以让原型对象重新指向原来的构造函数

一般情况下,对象的方法都在构造函数的原型对象中设置。

如下面这样

// 为构造方法的原型对象中添加speak 方法
Person.prototype.speak=function(){
	console.log('人类说话')
}

但是如果加入多个方法,使用上面的方式就比较麻烦

function Star(name, gender) {
    this.name = name
    this.gender = gender
}
// 像原型添加sing方法
Star.prototype.sing = function () {
	console.log('红星闪闪放光彩');
}
// 向原型添加 dance 方法
Star.prototype.dance = function () {
	console.lo('魔鬼的步伐')
}
// 向原型添加fly
Star.prototype.fly = function () {
	console.log('上了飞机就拖鞋')
}

像上面这样,如果要添加多个方法,我们可以给原型对象采取对象形式赋值,如下面的代码

Star.prototype = {
    sing: function () {
    	console.log('红星闪闪放光彩');
    },
    dance: function () {
    	console.lo('魔鬼的步伐')
    },
    fly: function () {
    	console.log('上了飞机就拖鞋')
    }
}

但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了

此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。

结果

总结:如果我们修改了原来的原型对象,给原型对象赋值的是一个对象,则必须手动的利用constructor指回原来的构造函数如:

完整代码

function Star(name, gender) {
    this.name = name
    this.gender = gender
}        
Star.prototype = {
    construcotr:Star,
    sing: function () {
        console.log('红星闪闪放光彩');
    },
    dance: function () {
        console.lo('魔鬼的步伐')
    },
    fly: function () {
        console.log('上了飞机就拖鞋')
    }
}
console.log(Star.prototype)

1.7原型链

​ 每一个实例对象又有一个__proto__属性,指向的构造函数的原型对象,构造函数的原型对象也是一个对象,也有__proto__属性,这样一层一层往上找就形成了原型链。

可以类比基类,基类就是Object

其实所有的自定义的或者系统内置的构造函数或者类的最顶级的对象原型都是 Object

1.8构造函数实例和原型对象三角关系

1.构造函数的prototype属性指向了构造函数原型对象,构造函数的原型对象的constructor属性指向了构造函数

function Star(name, age) {
    this.name = name
    this.age = age
}
// 获取输出构造方法的原型对象
console.log(Star.prototype)
// 获取并输出原型对象的构造函数
console.log(Star.prototype.constructor)
// 证明原型对象的constructor属性确实获取的是对应的构造函数
console.log(Star.prototype.constructor===Star)

2.实例对象是由构造函数创建的,实例对象的__proto__属性指向了构造函数的原型对象

// 实例对象由构造函数创建
var s1=new Star('肖战',18)
// 实例对象的__proto__属性指向了对应构造函数的原型对象
console.log(s1.__proto__)

3.构造函数的原型对象的constructor属性指向了构造函数,实例对象的原型就是构造函数的原型对象,此对象中有constructor属性也指向了构造函数

重要说明:上面所说的理论同样适用于ES6中

class Star {
    constructor(name){
    this.name=name
    }
}
var s=new Star('yhb')
console.log(Star.prototype)
console.log(s.__proto__)

1.9原型链和成员的查找机制

任何对象都有原型对象,也就是prototype属性,任何原型对象也是一个对象,该对象就有__proto__属性,这样一层一层往上找,就形成了一条链,我们称此为原型链;

当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
如果没有就查找它的原型(也就是 __proto__指向的 prototype 原型对象)。
如果还没有就查找原型对象的原型(Object的原型对象)。
依此类推一直找到 Object 为止(null)。
__proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。

1.10原型对象中this指向

构造函数中的this和原型对象的this,都指向我们new出来的实例对象

1、构造函数中的this指向的就是new出来的对象

function Star(name, age) {
    this.name = name
    this.age = age
    console.log(this)
}
var s1=new Star('李白',20)
var s2=new Star('杜甫',17)

2、构造函数的原型对象上的this指向的也是new出来的对象

function Star(name, age) {
    this.name = name
    this.age = age
    console.log(this)
}
Star.prototype.sing=function(){
    console.log(this)
}
var s1=new Star('李白',20)
var s1=new Star('杜甫',17)

通过下面的代码也可以比较两个到底是不是都只想了 new 出来对象

 function Star(name, age) {
     this.name = name
     this.age = age            
 }
 var that = null
 // 构造函数的原型对象中的this
 Star.prototype.sing = function () {
 	that = this
 }
 var s1 = new Star('李白', 20)
 // 韩炳旭说:sing方法一定要调用,否则无法执行赋值操作
 s1.sing()
 console.log(that === s1)

1.11通过原型为数组扩展内置方法

查看数组的原型

 console.log(Array.prototype);

为数组扩展和一个方法,可以计算数组中元素的和

 var numbers = new Array(1, 2, 3, 4, 5)

        // 传统方法求和
        // var sum = 0
        // for (var i = 0; i < numbers.length; i++) {
        //     sum += numbers[i]
        // }

        // 通过改变原型
        Array.prototype.sum = function () {
            var sum = 0
            for (var i = 0; i < this.length; i++) {
                sum += this[i]
            }
            return sum
        }
        console.log(numbers.sum())

        // 再创建一个数组,使用[]方式创建的也是Array类的实例
        var arr=[2,3,4,7]
        console.log(arr.sum())
        console.log(Array.prototype)

2.继承

在ES6之前,没有extends 关键字实现类的继承,需要通过构造函数+原型对象的方式模拟实现继承,这种方式叫做组合继承

2.1call()

  • call()可以调用函数
  • call()可以修改this的指向,使用call()的时候 参数一是修改后的this指向,参数2,参数3(普通参数)…使用逗号隔开连接

下面代码单纯的调用函数

function f1() {
	console.log(this) //window
}
f1.call()

下面代码修改this的指向

  function f1() {
            /*
            *1)默认情况下,this=window
            *2)使用call修改后,this=p
            */
            console.log(this) 
        }
        f1.call() // 输出window
        // 创建对象p
        var p={}
        // 将函数f1的this指向修改为对象p
        f1.call(p) // 输出p

上面call 方法的第一个参数就是this引用的新对

以上是关于构造函数原型继承原来这么简单?来吧!深入浅出的主要内容,如果未能解决你的问题,请参考以下文章

javascripe之继承

js原型继承的几种方法

原型链的继承都发生在构造函数上

原型,原型链,继承

深入理解 Javascript 面向对象编程(转)

JS原型和继承