构造函数、原型、原型链、继承
JS里一切皆对象,对象是“无序属性的集合,其属性值可以是数据或函数”。
事实上,所有的对象都是由函数创建的,而常见的对象字面量则只是一种语法糖:
// let user = {name: 'paykan', age: 29} ,等同于:
let user = new Object(); user.name = 'paykan'; user.age = 29;
//let list = [1, 'hi', true],等同于:
let list = new Array(); list[0] = 1; list[1] = 'hi'; list[2] = true;
对象的特性
-
每个对象都有
constructor
,用来表明是谁创建了它。 -
每个对象都有一个
__proto__
属性,该属性是一个对象,被称为原型对象,原型对象有一个constructor
属性,指向创建对象的那个函数(obj.constructor === obj.__proto__.constructor
) -
在对象上访问一个属性或方法时,会先从该对象查找,若找不到就去原型对象上找。
所以一个简单的字符串也有若干属性和方法,因为它们来自原型对象:
let str = '123-456-789'; str.split === str.__proto__.split; //true
-
每个函数只要被创建就会有一个prototype属性,它的值就是原型对象(所以访问原型对象有两条途径:函数的prototype、实例对象的
__proto__
)。 -
原型对象可以被修改,而对原型对象的修改可以立即反映到实例对象上。
创建对象
工厂模式
function Person(name){ return {name: name} };
let paykan = Person('paykan')
这里的paykan对象其实并非Person函数创建的,因为该函数只是使用了对象字面量——调用了Object()
。这种方式只是封装了使用对象字面量的过程,但并非完全无用。
构造函数模式
function Person(name){
this.name = name;
this.say = function(){
return this.name
}
};
let man = new Person('paykan');
-
这里有三个特点:
-
函数内部没有创建对象;
-
属性和方法直接传递给了this对象;
-
使用new关键字来调用。任何一个函数,只要使用了new关键字,它就成了构造函数。
-
-
使用new关键字调用函数时发生了以下事情:
-
创建新对象
-
将函数的作用域赋给新对象,从而使得this指向了该对象
-
执行函数代码(为新对象添加属性和方法)
-
返回新对象
-
这里的man对象才算是真正由Person函数创建的了:
man.constructor; //ƒ Person(name){ this.name = name ... }
man.__proto__.constructor; //ƒ Person(name){ this.name = name ... }
man.__proto__.constructor === man.constructor; //true
构造-原型组合模式
根据对象的特性,对象上没有的属性会在原型对象中寻找,所以可以把公共的属性和方法给到原型对象上去。
可以通过函数的prototype或者对象的__proto__
来实现:
function Person(name){ this.name = name };
let man = new Person('paykan');
Person.prototype.nation = 'Chinese';
man.nation; //Chinese
man.__proto__.greeting = function(){
return 'Hi, there';
}
man.greeting(); //Hi, there
动态原型模式
这种模式把给对象添加属性以及给原型添加属性的动作都放到了构造函数里,原型的属性只在创建第一个对象实例时添加,以后就会被跳过。
function Person(name){
this.name = name;
if(!this.nation){ Person.prototype.nation = 'Chinese' };
};
原型链
函数被创建后prototype指向了默认的原型对象,如果使用new调用该函数来生成一个对象,就会形成函数、对象、原型之间的三角关系:
此时如果让实例对象指向另一个构造函数的实例对象,这个关系就变成了这样:
实例对象A和实例对象B被一个__proto__
属性链接起来了,这已经是一个具有两个节点的链条了,称为原型链。只需要修改函数的prototype的指向或者实例对象的__proto__
的指向,就可以产生原型链。
实际上,由于原型对象B是由Object()函数创建的,而Object()函数的prototype的__proto
指向的是null
,所以一条原型链的起点是实例对象,终点是null
,中间由__proto__
链接。
如果在实例对象A上访问某个属性或方法,JS会从实例对象A开始沿着原型链层层查找,直到遇见null
。
继承
有了原型链的概念就可以开始实现继承了,最基本的模式就是修改原型对象:
function Father(){
this.say = function(){return this.name}
}
function Child(name){
this.name = name;
}
Child.prototype = new Father();
let man = new Child('jack');
man.say(); //'jack'
由于对原型的修改会立即反映到所有实例上,实例对象会互相影响,而且在调用Child函数时无法给Father函数传参,所以我们需要更加实用的继承方式。
省略分析推导过程,这里只介绍最实用和可靠的实现继承的方式:组合继承,为了方便描述,引入“父类函数”和“子类函数”这两个概念:
//父类函数
function Father(name, age){
this.name = name;
this.age = age;
}
//在父类函数的prototype上定义方法
Father.prototype.say = function(){
return `name: ${this.name}, age: ${this.age}, intrest: ${this.intrest}`
}
//子类函数
function Child(name, age, intrest){
this.intrest = intrest;
Father.call(this, name, age); //在子类对象上调用父类构造函数,并为之传参
}
//设置子类函数的prototype为父类的实例
Child.prototype = new Father();
//修改constructor属性,使之指向子类,此非必需,但可以让实例对象知道是谁创建了它
Child.prototype.constructor = Child;
let man = new Child('paykan', 29, 'coding');
man.say(); //"name: paykan, age: 29, intrest: coding"
这种继承方式有以下几个特点:
- 子类继承了父类所设定的属性,但每个实例对象都可以有自己的属性值,不会互相影响
- 子类共享了父类定义的方法,因为方法是在父类的prototype上的,所以不会在每个实例对象上创建一遍
- 如果有哪个属性是可以被所有实例对象共享的,可以设置到父类的prototype上去。
总之利用原型链实现可靠继承的步骤是:
- 在父类函数内设置通用的属性
- 在子类函数内调用父类函数,并设置特有的属性
- 修改子类函数的prototype,以继承父类
- 修改子类函数的prototype.constructor,纠正对象识别问题
- 使用new关键字调用子类函数,传递所有必需的参数