一、构造函数 1.1、概述 在面向对象编程语言中,比如说Java语言,是有类的概念的,类就是对象的模板,对象就是类的实例。但是在ES6之前,JavaScript并没有类的概念。
1 # 在 ES6 之前 ,对象不是基于类创建的,而是用一种称为【构造函数】的特殊函数来定义对象和它们的特征。换句话说,使用 构造函数 来去模拟类。
1.2、创建对象的方式 在JavaScript中,创建对象的方式有三种:
字面量对象
new Object()
自定义构造函数
1.3、构造函数 1.3.1、概述 构造函数 是一种特殊的函数,主要用来初始化对象 ,即为对象成员变量赋初始值,它总与 new 一起使用。我们可以把对象中一些公共的属性和方法抽取出来,然后封装到这个函数里面。
在 JS 中,使用构造函数时要注意以下两点:
构造函数用于创建某一类对象,其首字母要大写
构造函数要和 new 一起使用才有意义
1.3.2、new 的含义 new 在执行时会做四件事情:
在内存中创建一个新的空对象。
让 this 指向这个新的对象。
执行构造函数里面的代码,给这个新对象添加属性和方法。
返回这个新对象(所以构造函数里面不需要 return )。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <body > <script > function Star (uname, age ) { this .uname = uname; this .age = age; this .sing = function ( ) { console .log ("我会唱歌" ); } } var ldh = new Star ('刘德华' , 20 ); var zxy = new Star ('张学友' , 21 ); ldh.sing (); zxy.sing (); </script > </body >
1.3.3、静态成员/实例成员 JavaScript 的构造函数中可以添加一些成员,可以在构造函数本身上添加,也可以在构造函数内部的 this 上添加。通过这两种方式添加的成员,就分别称为静态成员 和实例成员 。
静态成员:在构造函数本上添加的成员称为==静态成员,只能由构造函数本身来访问。 ==
实例成员:在构造函数内部创建的对象成员称为==实例成员,只能由实例化的对象来访问。 ==
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <body > <script > function Star (uname, age ) { this .uname = uname; this .age = age; this .sing = function ( ) { console .log ("我会唱歌" ); } } var ldh = new Star ('刘德华' , 23 ); console .log (ldh.uname ); ldh.sing (); console .log (Star .uname ); </script > </body >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <body > <script > function Star (uname, age ) { this .uname = uname; this .age = age; this .sing = function ( ) { console .log ("我会唱歌" ); } } Star .sex = '男' ; console .log (Star .sex ); </script > </body >
二、原型 2.1、构造函数创建对象的问题 构造函数创建对象该方法很好用,但是存在浪费内存 的问题,更好的方式是:属性属于对象的,方法是共享的。
1 1. 我们希望所有的对象使用同一个函数,这样就比较节省内存,那么我们要怎样做呢?
2.2、构造函数原型prototype 2.2.1、说明 1 # 明确的说明:构造函数通过原型prototype分配的函数是所有对象所共享的。
2.2.2、概述 1 2 3 4 5 1. JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象。注意这个 prototype 就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。# 我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。 # 简单来说:原型的作用就是用来共享方法的。
2.2.3、代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <body > <script > function Star (uname, age ) { this .uname = uname; this .age = age; } Star .prototype .sing = function ( ) { console .log ("我会唱歌" ); } var ldh = new Star ('刘德华' , 20 ); var zxy = new Star ('张学友s' , 21 ); ldh.sing (); zxy.sing (); console .log (ldh.sing == zxy.sing ); console .dir (Star ); </script > </body >
1 # 总结:我们的公共属性定义到构造函数里面, 公共的方法我们放到原型对象身上。
2.3、对象原型proto 2.3.1、问题分析 通过刚才2.2.3
案例,可以发现,我们只是向构造函数的原型对象身上添加了sing
方法,但是呢,对于通过构造函数所创建出来的实例对象ldh
、 zxy
也能够使用sing
方法,这个是为什么呢?
原因就是在于每个实例对象身上都会有一个__proto__
的属性。
2.3.2、说明 1 # 实例对象都会有一个属性 __proto__ 指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 __proto__ 原型的存在。
proto 对象原型和原型对象 prototype 是等价的。
proto 对象原型的意义就在于为对象的查找机制提供一个方向,或者说一条路线,但是它是一个非标准属性,因此实际开发中,不可以使用这个属性,它只是内部指向原型对象 prototype
2.3.3、代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <body > <script > function Star (uname, age ) { this .uname = uname; this .age = age; } Star .prototype .sing = function ( ) { console .log ("我会唱歌" ); } var ldh = new Star ('刘德华' , 20 ); var zxy = new Star ('张学友s' , 21 ); ldh.sing (); zxy.sing (); console .log (ldh); console .log (ldh.__proto__ === Star .prototype ); </script > </body >
2.4、constructor 构造函数 2.4.1、说明 对象原型(proto) 和构造函数(prototype)原型对象 里面都有一个属性 constructor 属性 ,constructor 我们称为构造函数,因为它指回构造函数本身。
constructor 主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。
一般情况下,对象的方法都在构造函数的原型对象中设置 。如果有多个对象的方法,我们可以给原型对象采取对象形式赋值,但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了。此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。
2.4.2、案例1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <body > <script > function Star (uname, age ) { this .uname = uname; this .age = age; } Star .prototype .sing = function ( ) { console .log ("我会唱歌" ); }; Star .prototype .movie = function ( ) { console .log ("我会拍电影" ); }; var ldh = new Star ('刘德华' , 20 ); ldh.sing (); ldh.movie (); console .log (Star .prototype ); console .log (ldh.__proto__ ); console .log (Star .prototype .constructor ); console .log (ldh.__proto__ .constructor ); </script > </body >
2.4.3、案例2 2.4.2案例1
属于很标准的代码,但是可以有改进的地方,如果我们需要往原型对象身上添加多个方法,那么每次都需要Star.prototype.方法
这种方式来去添加。显然很麻烦的,那么更好的做法是这样的,将这些方法封装起来,以对象的形式赋值给原型对象。伪代码如下:
1 2 3 4 5 6 7 8 Star .prototype = { sing : function ( ) { }, movie : function ( ) { } }
1 # 注意:这种方式并不是向原型对象身上添加sing方法和movie方法,而是将原型对象重新赋值给了一个新的对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <body > <script > function Star (uname, age ) { this .uname = uname; this .age = age; } Star .prototype = { sing : function ( ) { console .log ("我会唱歌" ); }, movie : function ( ) { console .log ("我会拍电影" ); } } var ldh = new Star ('刘德华' , 20 ); ldh.sing (); ldh.movie (); console .log (Star .prototype ); console .log (ldh.__proto__ ); console .log (Star .prototype .constructor ); console .log (ldh.__proto__ .constructor ); </script > </body >
2.4.4、案例3 2.4.3案例2
发现,查询构造函数身上的原型对象的信息再也找不到constructor
属性了,即不知道这个原型对象所引用的构造函数是哪个了,那该怎么解决呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <body > <script > function Star (uname, age ) { this .uname = uname; this .age = age; } Star .prototype = { constructor : Star , sing : function ( ) { console .log ("我会唱歌" ); }, movie : function ( ) { console .log ("我会拍电影" ); } } var ldh = new Star ('刘德华' , 20 ); ldh.sing (); ldh.movie (); console .log (Star .prototype ); console .log (ldh.__proto__ ); console .log (Star .prototype .constructor ); console .log (ldh.__proto__ .constructor ); </script > </body >
2.5、构造函数、实例对象、原型对象三者之间的关系 2.5.1、图示
三、原型链 3.1、引入 刚才讲解了构造函数原型对象至此已经完毕了,现在再思考一个问题,构造函数原型对象,它本身也是一个对象,既然是对象,那么也应该有一个__proto__
属性,那么这个属性又指向的是哪个原型对象呢?
答案:指向的是Object原型对象。
1 2 3 4 # 注意 1. 只要是对象,就有__proto__ 原型, 指向原型对象 2. 我们Star原型对象里面的__proto__ 原型指向的是 Object.prototype 3. 我们Object.prototype原型对象里面的__proto__ 原型 指向为 null
3.1.1、代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <body > <script > function Star (uname, age ) { this .uname = uname; this .age = age; } Star .prototype .sing = function ( ) { console .log ("我会唱歌" ); } var ldh = new Star ('刘德华' , 20 ); ldh.sing (); console .log (Star .prototype .__proto__ == Object .prototype ); console .log (Object .prototype .__proto__ ); </script > </body >
3.2、对象成员查找机制(规则) 3.2.1、说明
当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
如果没有就查找它的原型(也就是 proto 指向的 prototype 原型对象)。
如果还没有就查找原型对象的原型(Object的原型对象)。
依此类推一直找到 Object 为止(null)。
proto 对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。
3.2.2、案例1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <body > <script > function Star (uname, age ) { this .uname = uname; this .age = age; } Star .prototype .sex = '男' ; var ldh = new Star ('刘德华' , 20 ); console .log (ldh.sex ); </script > </body >
3.2.3、案例2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <body > <script > function Star (uname, age ) { this .uname = uname; this .age = age; } Star .prototype .sex = '男' ; var ldh = new Star ('刘德华' , 20 ); ldh.sex = '女' ; console .log (ldh.sex ); </script > </body >
3.2.4、案例3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <body > <script > function Star (uname, age ) { this .uname = uname; this .age = age; } Star .prototype .sex = '男' ; var ldh = new Star ('刘德华' , 20 ); console .log (ldh.toString ()); </script > </body >
3.3、原型对象this指向 3.3.1、说明 1 2 1. 构造函数中的 this 指向我们实例对象。2. 原型对象里面放的是方法, 这个方法里面的this 指向的是 这个方法的调用者, 也就是这个实例对象。
3.3.2、案例1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <body > <script > var that = null ; function Star (uname, age ) { this .uname = uname; this .age = age; that = this ; } var ldh = new Star ('刘德华' , 20 ); console .log (ldh === that); </script > </body >
3.3.3、案例2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <body > <script > var that = null ; function Star (uname, age ) { this .uname = uname; this .age = age; } Star .prototype .sing = function ( ) { console .log ('我会唱歌' ); that = this ; } var ldh = new Star ('刘德华' , 20 ); ldh.sing (); console .log (ldh === that); </script > </body >
3.4、扩展内置对象 3.4.1、说明 可以通过原型对象,对原来的内置对象进行扩展自定义的方法。比如给数组增加自定义求和的功能。这样的话,通过给数组的原型对象添加方法,那么当我们去创建数组对象的时候,也就拥有了该方法。
比如:我对数组Array
的原型对象添加sum
求和的方法,那么new Array()
创建数组实例对象的时候,也会拥有该sum
方法。
伪代码如下:
1 2 3 4 5 6 7 8 9 10 <body > <script > Array .prototype .sum = function ( ) { } var arrs = new Array (); arrs.sum (); </script > </body >
3.4.2、代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <body > <script > Array .prototype .sum = function ( ) { var result = 0 ; for (var i = 0 ; i < this .length ; i++) { result = result + this [i]; } return result; } var arrs = new Array (1 , 2 , 3 , 4 , 5 ); console .log (arrs.sum ()); </script > </body >
3.4.3、注意问题 刚刚讲到,向原型对象中添加方法,其实可以将添加的方法用对象的形式封装起来,那么将这个对象赋值给原型对象,如果要这样做,该怎么写呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <body > <script > Array .prototype = { sum : function ( ) { var result = 0 ; for (var i = 0 ; i < this .length ; i++) { result = result + this [i]; } return result; } }; var arrs = new Array (1 , 2 , 3 , 4 , 5 ); console .log (Array .prototype ); console .log (arrs.sum ()); </script > </body >
3.4.4、总结 会发现,即使我们为内置对象Array
的原型对象重新赋值了一个新的对象,发现,还是没有的,就说明:
1 # 注意:数组和字符串内置对象不能给原型对象覆盖操作 Array.prototype = {} ,只能是 Array.prototype.xxx = function(){} 的方式。
四、继承 4.1、学习准备 4.1.1、注意点 ES6之前并没有给我们提供 extends 关键字实现继承。我们可以通过构造函数+原型对象 模拟实现继承,被称为组合继承 。
4.1.2、call方法 call()
方法的学习。该方法的作用是可以改变一个函数内部this的指向。
1 2 3 4 5 # 语法 fun.call(thisArg, arg1, arg2, ...) 1. thisArg :当前调用函数 this 的指向对象 2. arg1,arg2:传递的其他参数
4.1.3、案例1 1 2 3 4 5 6 7 8 9 10 11 <body > <script > function fn (x, y ) { console .log ("x + y =" + (x + y)); console .log (this ); } fn (10 , 20 ); </script > </body >
4.1.4、案例2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <body > <script > function fn (x, y ) { console .log ("x + y = " + (x + y)); console .log (this ); } var person = { name : 'HelloWorld' } fn.call (person, 1 , 3 ); </script > </body >
4.2、借用构造函数继承父类型属性 4.2.1、核心原理 1 # 通过 call() 把父类型的 this 指向子类型的 this ,这样就可以实现子类型继承父类型的属性。
4.2.2、案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <body > <script > function Father (uname, age ) { this .uname = uname; this .age = age; } function Child (uname, age, password ) { Father .call (this , uname, age); this .password = password; } var child = new Child ('HelloWorld' , 20 , '123456' ); console .log (child.uname ); console .log (child.age ); console .log (child.password ); </script > </body >
4.3、借用构造函数继承父类型方法 4.3.1、说明 1 # 一般情况下,对象的方法都在构造函数的原型对象中设置,通过构造函数无法继承父类方法。
4.3.2、代码错误实现1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <body > <script > function Father (uname, age ) { this .uname = uname; this .age = age; } Father .prototype .sing = function ( ) { console .log ('---我会唱歌---' ); }; function Child (uname, age, password ) { Father .call (this , uname, age); this .password = password; } var child = new Child ('HelloWorld' , 20 , '123456' ); child.sing (); </script > </body >
1 1. 说明: 仅仅向父构造函数的原型对象上添加方法,子类是不能继承的。
4.3.3、代码实现2 4.3.2代码错误实现1
会发现,单纯的向父构造函数的原型对象添加方法是不行的,所以我们自然的可以想到,那么将父构造函数的原型对象再赋值给子构造函数的原型对象,不就可以了嘛,形如这样的:
1 Child .prototype = Father .prototype
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <body > <script > function Father (uname, age ) { this .uname = uname; this .age = age; } Father .prototype .sing = function ( ) { console .log ('---我会唱歌---' ); }; function Child (uname, age, password ) { Father .call (this , uname, age); this .password = password; } Child .prototype = Father .prototype ; var child = new Child ('HelloWorld' , 20 , '123456' ); child.sing (); </script > </body >
4.3.4、代码实现3 4.3.3代码实现2
发现,确实解决了问题,但是有一个不好的地方,就是如果子构造函数的原型对象身上本身也有方法呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <body > <script > function Father (uname, age ) { this .uname = uname; this .age = age; } Father .prototype .sing = function ( ) { console .log ('---我会唱歌---' ); }; function Child (uname, age, password ) { Father .call (this , uname, age); this .password = password; } Child .prototype = Father .prototype ; Child .prototype .run = function ( ) { console .log ('跑步' ); }; var child = new Child ('HelloWorld' , 20 , '123456' ); child.sing (); child.run (); console .log (Father .prototype ); </script > </body >
总结:本身run
方法是属于子类身上的,但是现在由于子构造函数的原型对象指向了父构造函数的原型对象,所以导致父原型对象身上也有run
方法,这个是不太合适的。
4.3.5、正确代码实现1 1 2 3 4 5 6 # 思路 1. 将子类所共享的方法提取出来,让子类的 prototype 原型对象 = new 父类() 2. 本质:子类原型对象等于是实例化父类,因为父类实例化之后另外开辟空间,就不会影响原来父类原型对象 3. 将子类的 constructor 从新指向子类的构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <body > <script > function Father (uname, age ) { this .uname = uname; this .age = age; } Father .prototype .sing = function ( ) { console .log ('---我会唱歌---' ); }; function Child (uname, age, password ) { Father .call (this , uname, age); this .password = password; } Child .prototype = new Father (); Child .prototype .run = function ( ) { console .log ('跑步' ); }; var child = new Child ('HelloWorld' , 20 , '123456' ); child.sing (); child.run (); </script > </body >
4.3.6、正确代码实现1改进 4.3.5正确代码实现1
确实解决了父构造函数的原型对象身上不会再有子类所特有的方法了,但是以前说过,如果把一个新的对象直接复制给构造函数的原型对象,那么这个构造函数的原型对象的constructor
属性就不再指向原来的构造函数了,而是指向新创建的对象了,可以尝试看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <body > <script > function Father (uname, age ) { this .uname = uname; this .age = age; } Father .prototype .sing = function ( ) { console .log ('---我会唱歌---' ); }; function Child (uname, age, password ) { Father .call (this , uname, age); this .password = password; } Child .prototype = new Father (); Child .prototype .run = function ( ) { console .log ('跑步' ); }; var child = new Child ('HelloWorld' , 20 , '123456' ); console .log (Child .prototype .constructor ); </script > </body >
正确的方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <body > <script > function Father (uname, age ) { this .uname = uname; this .age = age; } Father .prototype .sing = function ( ) { console .log ('---我会唱歌---' ); }; function Child (uname, age, password ) { Father .call (this , uname, age); this .password = password; } Child .prototype = new Father (); Child .prototype .constructor = Child ; Child .prototype .run = function ( ) { console .log ('跑步' ); }; var child = new Child ('HelloWorld' , 20 , '123456' ); console .log (Child .prototype .constructor ); </script > </body >