(一) JavaScript原型
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针
名词解释: 指针后台语言说法, 在前端就是属性的意思<script> // 1. 每个构造函数都有一个prototype属性,指向了它的原型对象(打开控制台查看试试) // 2. 原型对象都存在一个属性constructor指向了构造函数,例如Date构造函数: console.log(Array.prototype); </script>
每个实例都包含一个指向原型 对象的内部指针
名词解释: 实例就是构造函数new出来的对象<script> // a.每个实例都存在一个属性__proto__, 指向了原型对象 var date = new Date(); var arr = new Array(1, 2, 3); console.log(date.__proto__); console.log(arr.__proto__); // 根据第1点每个构造函数都有一个原型对象, 思考问题: 构造器的prototype和实例的__proto是什么关系 console.log(Array.prototype === arr.__proto__); </script>
实例继承原型对象的所有方法
<script> // 1. 创建数组对象 var arr = [1, 2, 3, 4]; // 2. arr就会拥有length属性,push,pop等方法,请问它的属性和方法是它的原型对象给的 console.log(arr.length); arr.push(5); console.log(arr); // 3. 自定义一个构造函数, 给它的构造器和原型对象添加属性或方法 function MadeCat() { this.name = '猫'; } MadeCat.prototype.color = '黑色'; MadeCat.prototype.sayName = function() { console.log(this.name); } var cat = new MadeCat(); console.log('cat', cat); </script>
构造函数, 原型对象,实例三者关系的比喻(看图)
实例、构造函数、原型对象三者的关系就好像儿子、妈妈、爸爸的关系
1.每一个妻子(构造函数)都有一个丈夫(原型对象),丈夫(原型对象)身上有一个结婚证,结婚证上那个女人的名字就是他妻子(构造函数)
2.小孩是由妈妈生产出来的(就好像实例是由构造对象new出来一样),每个小孩(实例)都有一个爸爸(原型对象)
结论:
1.构造函数.prototype === 原型对象
2.实例.__proto__ === 原型对象
3.构造函数.prototype === 实例.__proto__; // 原型对象是同一个
(二) JavaScript原型链
- 原型和实例的关系:
实例内容有一个指针指向了原型对象(换一种说法就是实例有一个属性指向了原型对象)
2. 原型链:// 创建日期实例(对象) var date = new Date(); // 日期实例有一个属性__proto__指向了原型对象 console.log('原型对象=',date.__proto__);
实例、构造函数、原型对象关系图
结论: 根据上图, 存在以下关系: ```javascript var arr = [1,2,3]; var date = new Date(); var obj = {name:’zs’};
// 1.实例.proto===构造函数.prototype date.proto === Date.prototype; arr.proto === Array.prototype; cat.proto === MadeCat.prototype; obj.proto === Object.prototype;
// 2.终极原型都一样, 都是Object的原型对象 date.proto.proto === Object.prototype; arr.proto.proto === Object.prototype; cat.proto.proto === Object.prototype;
// 3. 根据第2点得到 date.proto.proto === arr.proto.proto; arr.proto.proto === cat.proto.proto; date.proto.proto === cat.proto.proto;
2. (2) 原型链概念: 假如我们让原型对象等于另一个类型的实例, ,此时的原型对象将包含一个指向另一个原型对象的指针,假如另一个原型对象又是另一个类型的实例, 那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。
```javascript
// 举例说明
// 动物类型构造器
function MadeAnimal() {
this.type = '动物';
}
// 创建一个动物的实例
var animal = new MadeAnimal();
// 猫类型构造器
function MadeCat(name,age) {
this.name = name;
this.age = age;
}
// 让猫的原型对象等于动物类型的一个实例
MadeCat.prototype = animal;
var cat = new MadeCat('小花猫',2);
// 就会存在以下的关系:
(1)猫的实例cat存在一个属性__proto__指向了猫的原型对象,
(2)因为猫的原型对象是动物的一个实例animal,既然它是个实例,则animal实例就会存在一个__proto__属性,指向了动物的原型对象
(3)实际上,动物的原型对象还存在一个__proto__属性,它指向了Object的原型对象,所以以下的等式是成立的
cat.__proto__ 猫的原型对象是animal
cat.__proto__.__proto__ 这是animal的原型对象
cat.__proto__.__proto__.__proto__ animal原型对象内部还存在一个__proto__属性,指向了Object.prototype
cat.__proto__.__proto__.__proto__ === Object.prototype; //
问到原型和原型链这么回答:
原型: 原型指的是原型对象, 每一个构造函数都会有一个对应的原型对象, 同时原型对象也存在一个指针(属性),指向了它的构造器.
原型链:
- 我们创建的每一个实例, 都存在一个内部指针(属性,叫
__proto__
), 指向了它的原型对象, 并且会继承原型对象的属性和方法 - 假如我们让原型对象等于另一个类型的实例, ,此时的原型对象将包含一个指向另一个原型对象的指针,假如另一个原型对象又是另一个类型的实例, 那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。
ps: 第2点其实是第1点的重复
(三) JavaScript继承
需要掌握call,apply和bind的知识
JavaScript5.1版本常见的有5中继承方式: 原型继承, 借用构造函数继承, 寄生式继承, 组合继承, 寄生组合式继承
原型继承: 让子类的原型对象=父类的实例
缺点:- 父类的引用类型属性会被所有子类实例共享,任何一个子类实例修改了父类的引用类型属性,其他子类实例都会受到影响
创建子类实例的时候,不能向父类传参
<script> // 1. 父类 function Father() { this.type = 'father'; this.names = ['tom', 'kevin']; } // 2.给父类Father的原型对象添加getName方法 Father.prototype.getName = function() { console.log(this.names); } // 3.创建父类的实例 var father = new Father(); // console.log(father); // 2. 子类 function Child() { } // 3. 让子类的原型对象等于父类的一个实例, 完成原型链继承 Child.prototype = father; // 4. 创建一个子类实例 var child = new Child(); console.log(child); // 5. 创建另外一个实例 var child2 = new Child(); console.log(child2); // 6. 缺点展示: 给其中一个实例的names添加一个成员 child2.names.push('zs'); console.log('child2.names', child2.names); console.log('child.names', child.names); // 不是引用类型,不受影响 child2.type = 'aaaa'; console.log('child2.type', child2.type); console.log('child.type', child.type); </script>
借用构造函数继承
在子类的构造函数中调用父类的构造器实现继承- 优点:
- 避免了引用类型属性被所有实例共享
- 可以向父类传参
缺点:
- 方法必须定义在构造函数中, 定义在父类原型对象上的方法无法继承
- 每创建一个实例都会创建一遍方法 ```javascript ```
寄生式继承
- 优点:
- 需要先传入一个对象, 再往对象上添加属性或方法, 就好像寄生一样(想象一下黄蜂是怎么养育后代的)
- 就是创建一个封装的函数来增强原有对象的属性,缺点跟借用构造函数一样,每个实例都会创建一遍方法 ```javascript
4. **组合式继承(第1种和第2种组合)**
- 既有原型继承,也借用了父类的构造函数
- 组合式继承方式唯一的缺点是,父类的构造函数会被调用两次
```javascript
<script>
// 1.创建父类
function Father(name, age) {
this.name = name;
this.age = age;
this.list = [1, 2, 3];
}
// 2.给父类的原型对象添加say方法
Father.prototype.say = function() {
console.log('hello');
}
// 3.创建子类
function Child(name, age) {
// 4.借用父类构造函数
Father.call(this, name, age);
}
// 5.让子类的原型对象等于父类的一个实例,这样子类就继承了父类的所有属性和方法,new Father时可以传参也可以不传
Child.prototype = new Father('张无忌', 50);
// 6.创建子类实例
var child = new Child('张三', 18);
console.log('child', child);
var child2 = new Child('李四', 20);
console.log('child', child2);
console.log(child.list === child2.list); //false,引用类型属性不会相互影响
</script>
寄生组合式继承(最完美的继承方式)
完美的继承方式应该是这样:- 属性定义在父类构造函数里, 并且这些属性被子类继承的时候不会相互影响
公共方法定义在父类的原型对象里, 子类可以共享这些方法
<script> // 1. 创建一个组合式继承 function Father(name, age) { this.name = name; this.age = age; this.list = [1, 2, 3]; } Father.prototype.say = function() { console.log(this.name); } function Child(name, age) { // 借用构造函数 Father.call(this, name, age); } // 2. 添加寄生式继承Object.create Child.prototype = Object.create(Father.prototype); // Child.prototype.__proto__ === Father.prototype; 产生原型的继承关系 console.log(Child.prototype.constructor === Child); // false, 因为寄生式继承破坏了原来的原型关系 // 3. 重新制定子类原型对象的构造函数 Child.prototype.constructor = Child; var child = new Child('张三', 18); console.log('child', child); </script>
(四) JavaScript继承总结
原型链继承实现步骤
- 创建一个父类(准确来说是父类构造函数)
- 创建一个子类
- 让子类的prototype = 父类的一个实例
(1)定义: 让子类的prototype等于父类的一个实例 (2)缺点: - 父类的引用类型属性会被所有子类实例共享,任何一个子类实例修改了父类的引用类型属性,其他子类实例都会受到影响 - 创建子类实例的时候,不能向父类传参
借用构造函数继承实现步骤
- 创建一个父类
- 创建一个子类
- 在子类的构造函数里使用call或者apply调用父类的构造函数
(1)定义: 在子类的构造函数中调用父类的构造函数实现继承 (2)优点: - 避免了引用类型属性被所有实例共享 - 可以向父类传参 (3)缺点: - 方法必须定义在构造函数中,定义在父类原型对象的方法子类无法继承 - 每创建一个实例都会创建一遍方法,也就是方法不能共享
寄生式继承实现步骤
- 创建一个用于继承的对象(可以称为父类对象)
- 创建一个构造方法
- 在构造方法里接收父类对象作为参数, 并调用用Object.crate,传入父类对象来创建一个新的对象
- 给新的对象创建所需要方法
- 返回新对象, 就能得到一个继承了父类对象所有属性和方法的对象
(1)定义: 传入一个对象作为寄生的对象,根据传入对象创建一个新对象,再往对象上添加属性或方法, 就好像寄生一样 (2)缺点: 跟借用构造函数一样,每个实例都会创建一遍方法,也就是方法不能共享
组合式继承(第1种和第2种组合)实现步骤
- 创建父类, 给父类的原型对象添加公共方法
- 创建子类
- 在子类中调用父类的构造函数, 传入所需要的参数(如果需要的话)
- 让子类的原型对象等于父类的一个实例(new一个父类的实例)
(1)定义: 既有原型继承,也借用了父类的构造函数,实现继承使得子类可以共享父类的方法,方法不需要创建多次,借用构造函数使得可以调用继承父类的属性 (2)组合式继承方式唯一的缺点是,父类的构造函数会被调用两次
寄生组合式继承(最完美的继承方式)
完美的继承方式应该具备以下特点
- 子类实例可以继承父类的属性(包含基本数据类型和引用数据类型的属性), 同时引用数据类型的属性不会相互影响
- 可以共享父类原型对象的方法, 这些方法只需要创建一遍
- 组合寄生式继承符合了以上条件, 它的实现步骤:
- 创建一个具备组合式继承父类和子类
- 使用Object.create方法,传入父类原型对象, 得到的新对象赋给子类的原型对象
- 重新制定子类的原型对象的构造函数为子类构造函数
(1)组合式继承已经满足了以下条件, 唯一的缺点就是父类构造函数会调用两次 (2)寄生式继承,使用Object.create让子类原型对象跟父类原型对象产生了关系,从而实现的继承,它不需要new一个父类的实例,所有,父类的构造器就不需要调用两次