# 面向对象和原型链

# 一、面向对象

# 1、创建对象的几种方式

(1) new Object()

var person = new Object();
person.name = "lisi";
person.age = 21;
person.family = ["lida","lier","wangwu"];
person.say = function(){
    alert(this.name);
}
1
2
3
4
5
6
7

(2) 字面量创建对象

var person = {
	name: "lisi",
	age: 21,
	family: ["lida", "lier", "wangwu"],
	say: function() {
		alert(this.name);
	}
};
1
2
3
4
5
6
7
8

以上两种方法在使用同一接口创建多个对象时,会产生大量重复代码,为了解决此问题,工厂模式被开发。

(3) 工厂模式

function createPerson(name, age, family) {
	var o = new Object();
	o.name = name;
	o.age = age;
	o.family = family;
	o.say = function() {
		alert(this.name);
	}
	return o;
}

var person1 = createPerson("lisi", 21, ["lida", "lier", "wangwu"]); //instanceof无法判断它是谁的实例,只能判断他是对象,构造函数模式都可以判断出
var person2 = createPerson("wangwu", 18, ["lida", "lier", "lisi"]);
console.log(person1 instanceof Object); //true
1
2
3
4
5
6
7
8
9
10
11
12
13
14

工厂模式解决了重复实例化多个对象的问题,但没有解决对象识别的问题(因为全部都是Object,不像Date、Array等,本例中,得到的都是o对象,对象的类型都是Object,因此出现了构造函数模式)。

(4) 构造函数模式

function Person(name, age, family) {
	this.name = name;
	this.age = age;
	this.family = family;
	this.say = function() {
		alert(this.name);
	}
}
var person1 = new Person("lisi", 21, ["lida", "lier", "wangwu"]);
var person2 = new Person("lisi", 21, ["lida", "lier", "lisi"]);
console.log(person1 instanceof Object); //true
console.log(person1 instanceof Person); //true
console.log(person2 instanceof Object); //true
console.log(person2 instanceof Person); //true
console.log(person1.constructor); //constructor属性返回对创建此对象的数组、函数的引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

对比工厂模式有以下不同之处:

  • 没有显式地创建对象

  • 直接将属性和方法赋给了 this 对象

  • 没有 return 语句

以此方法调用构造函数步骤 {

  • 创建一个新对象

  • 将构造函数的作用域赋给新对象(将this指向这个新对象)

  • 执行构造函数代码(为这个新对象添加属性)

  • 返回新对象

}

可以看出,构造函数知道自己从哪里来(通过 instanceof可以看出其既是Object的实例,又是Person的实例)。 构造函数也有其缺陷,每个实例都包含不同的Function实例( 构造函数内的方法在做同一件事,但是实例化后却产生了不同的对象,方法是函数 ,函数也是对象)

因此产生了原型模式

(5) 原型模式

function Person() {
}

Person.prototype.name = "lisi";
Person.prototype.age = 21;
Person.prototype.family = ["lida","lier","wangwu"];
Person.prototype.say = function(){
    alert(this.name);
};
console.log(Person.prototype);   //Object{name: 'lisi', age: 21, family: Array[3]}

var person1 = new Person();        //创建一个实例person1
console.log(person1.name);        //lisi

var person2 = new Person();        //创建实例person2
person2.name = "wangwu";
person2.family = ["lida","lier","lisi"];
console.log(person2);            //Person {name: "wangwu", family: Array[3]}
// console.log(person2.prototype.name);         //报错
console.log(person2.age);              //21
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

原型模式的好处是所有对象实例共享它的属性和方法(即所谓的共有属性),此外还可以设置实例自己的属性(方法)(即所谓的私有属性),可以覆盖原型对象上的同名属性(方法)

(6) 混合模式(构造函数模式+原型模式)

构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性

function Person(name,age,family){
    this.name = name;
    this.age = age;
    this.family = family;
}

Person.prototype = {
    constructor: Person,  //每个函数都有prototype属性,指向该函数原型对象,原型对象都有constructor属性,这是一个指向prototype属性所在函数的指针
    say: function(){
        alert(this.name);
    }
}

var person1 = new Person("lisi",21,["lida","lier","wangwu"]);
console.log(person1);
var person2 = new Person("wangwu",21,["lida","lier","lisi"]);
console.log(person2);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

可以看出,混合模式共享着对相同方法的引用,又保证了每个实例有自己的私有属性。最大限度的节省了内存。

(7)Object.create()

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

const person = {
  isHuman: false,
  printIntroduction: function () {
    console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
  }
};

const me = Object.create(person);

me.name = "Matthew"; // "name" is a property set on "me", but not on "person"
me.isHuman = true; // inherited properties can be overwritten

me.printIntroduction();
// expected output: "My name is Matthew. Am I human? true"
me.__proto__===person;  // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 2、类的继承

(1)借助构造函数+call/apply实现继承

// 借助构造函数实现继承
function Parent(argument) {
	this.name = 'parent';
}
Parent.prototype.say = function() {}

function Child(argument) {
	Parent.call(this); // 原理就是call/apply, call和apply改变了父类this中的指向,
	// 使this指向了子类,这样就可以把父类的属性挂载到子类里
	this.age = 11;
}
var child = new Child();
console.log(child); // Child {name: "parent", age: 11}
1
2
3
4
5
6
7
8
9
10
11
12
13

缺点是只能继承父类实例上的属性,无法继承原型链上的属性。

(2)借助原型链实现继承

// 借助原型链实现继承
function Parent1(argument) {
	this.name = 'parent';
	this.age = [1, 2, 3];
}

function Child1(argument) {
	this.name = 'child';
}
Child1.prototype = new Parent1(); // 将父类的实例赋值給子类的原型,这样子类就继承了父类
var child11 = new Child1();
console.log(child11) // Child1 {age: 11 , __proto__: Parent1}

/*******************原型链继承的缺点********************/
var child12 = new Child1();
child11.age.push(4); // 往其中一个实例的引用属性添加一个元素
console.log(child11.age, child12.age) // 会发现都是打印出  [1, 2, 3, 4]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

缺点:当父类有引用属性时,由于原型对象的特点,多个实例对象的__proto__都是同一个,而引用属性在new的时候不会开辟新的地址,所以当一个实例对象改变了引用属性的值时,另一个对象也会随之改变。

(3)结合构造函数和原型链的方式

// 组合方式,结合上面两种
function Parent3(argument) {
	this.name = 'parent';
	this.age = [1, 2, 3]
}
Parent3.prototype.say = function() {}

function Child3(argument) {
	Parent3.call(this); // 结合构造函数
	this.type = 'test';
}
Child3.prototype = new Parent3(); // 结合原型链
var child31 = new Child3();
var child32 = new Child3();
console.log(child31, child32);
child31.age.push(4);
console.log(child31.age, child32.age);. //  [1, 2, 3, 4]      [1, 2, 3]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这种方式可以解决前两种方式的问题。缺点:父类的构造函数会执行两次。

优化:把上面的

Child3.prototype = new Parent3();
1

换成

Child3.prototype = Parent3.prototype;
1

以上方法还是存在不足,因为只要是通过原型链继承来的对象,它的constructor打印出来都是父类Parent3,即无法确认child31实例是由父类创造的还是由子类创造的。 原因:Child3和父类Parent3共用了一个原型。Child本身没有constructor,由于继承了父类,就会把父类的constructor作为自己的。

解决方案:把上面的

Child3.prototype = Parent3.prototype;
1

换成

Child3.prototype = Object.create(Parent3.prototype); // 让Child3继承Parent3,
// 由于Object.create会返回一个新对象,该对象继承了Parent3,再让Child3去继承Parent3,
// 这样就起到了隔离并且继承的作用。
Child3.prototype.constructor = Child3; // 修改Child3的constructor
1
2
3
4

这样的话就是组合继承的完美写法了。









 


 
 






// 组合方式,完美写法
function Parent3(argument) {
	this.name = 'parent';
	this.age = [1, 2, 3];
}
Parent3.prototype.say = function() {}

function Child3(argument) {
+	Parent3.call(this); // 结合构造函数
	this.type = 'test';
}
+ Child3.prototype = Object.create(Parent3.prototype);
+ Child3.prototype.constructor = Child3; // 结合原型链
var child31 = new Child3();
var child32 = new Child3();
console.log(child31, child32);
child31.age.push(4);
console.log(child31.age, child32.age);. //  [1, 2, 3, 4]      [1, 2, 3]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 二、原型链

# 1、原型、构造函数、实例之间的关系

image

# 2、instanceof 的原理

作用是判断某个实例对象是否是某个构造函数的实例。原理就是判断实例对象的__proto__属性和构造函数的prototype是否引用了同一个地址。

var A = function() {
	this.name = 'xixi';
}
var a = new A();
console.log(a instanceof A); // true
console.log(a instanceof Object); // true
1
2
3
4
5
6

会发现a既是A的实例,也是Object的实例,为什么呢,因为instanceof只要是原型链上的都会返回true。

判断的原理是

a.__proto__ === A.prototype; // a是A的实例
A.prototype.__proto__ === Object.prototype; // A.prototype是Object的实例

// instance就会把原型链上的对象都认为是自己的实例
1
2
3
4

那么有什么办法判断a是A的实例还是Object的实例?

// 利用 constructor
a.__proto__.constructor === A; // true
a.__proto__.constructor === Object; // false
1
2
3

# 3、new运算符做了哪些事?

当我们用new运算符new一个构造函数产生一个实例时,比如说: var obj = new Func 时,其背后的步骤是这样的:

(1)创建一个继承自 Func.prototype 的新对象;

(2)执行构造函数 Func,执行的时候,相应的传参会被传入,同时上下文(this)会被指定为第一步创建的新实例;

(3)如果构造函数返回了一个“对象”(定义Func的时候,可能会返回一个对象,也有可能不返回),那么这个对象会取代步骤1中new出来的实例被返回。如果构造函数没有返回对象,那么new出来的结果为步骤1创建的对象。

注意:new Func 等同于new Func(),只能用在不传递任何参数的情况。

按照上述原理,写一段代码模拟new运算符的实现原理:

function new1(func) {
	var args = [].slice.call(arguments) // 拿到参数
	var newObj = Object.create(func.prototype) // 创建新对象,继承自fun.prototype
	var returnObj = func.apply(newObj, args.slice(1)) // 执行构造函数,返回一个对象或者非对象
	return typeof returnObj == 'object' ? returnObj : newObj
}

function Person(name, age) {
	this.name = name
	this.age = age
}

console.log(new1(Person, 'zls', 19))
1
2
3
4
5
6
7
8
9
10
11
12
13
Last Updated: 5/25/2020, 9:24:41 AM