一、构造函数
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 属性
如图所示 Foo
对象有一个原型对象 Foo.prototype
,其上有两个属性,分别是 constructor
和 __proto__
,其中 __proto__
已被弃用。
构造函数 Foo
有一个指向原型的指针,原型 Foo.prototype
有一个指向构造函数的指针 Foo.prototype.constructor
,这就是一个循环引用,即:
Foo.prototype.constructor === Foo; // true
2.2 __proto__
每个实例对象(object )都有一个隐式原型属性(称之为 __proto__
)指向了创建该对象的构造函数的原型。也就是指向了函数的 prototype
属性。
function Foo () {}
let foo = new Foo()
当 new Foo()
时,__proto__
被自动创建。并且
foo.__proto__ === Foo.prototype; // true
2.3 原型链
每个对象拥有一个原型对象,通过 __proto__
指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null
,这种关系被称为原型链(prototype chain)。根据定义,null
没有原型,并作为这个原型链中的最后一个环节。
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, 爱好抽烟, 喝酒, 烫头, 火锅
优点:
- 父类可以复用
缺点:
- 引用类型值的原型属性会被所有实例共享
- 不能给父类传递参数
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()); // 报错
优点:
- 因为是每次都调用了父类,所以不会子类生成的实例不会共享同一个实例。
- 可以给父类传参
缺点:
- 仅仅是借用了构造函数,方法只能在构造函数中定义,每次调用子类构造函数都会调用父类构造函数。
- 父类原型上定义的方法,对于子类无法找到。
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()
优点:
- 融合原型链继承和借用构造函数的优点
缺点:
- 父类调用两次,一次是用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));
缺点:
- 引用类型值的属性始终都会共享相应的值,就像使用原型模式一样
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));
优点:
- 根据一个对象克隆创建另一个对象,并增强对象
缺点:
- 引用类型值的属性始终都会共享相应的值
- 跟借用构造函数模式一样,每次创建对象都会创建一遍方法。
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);