js学习笔记之面向对象(对象、原型与继承三者的关系)
一、对象
1. 面向对象的特性:
- 封装:包括封装数据、封装实现、封装类型和封装变化,目的是将信息隐藏。
- 继承:类与类之间的关系(js中没有类的概念,但可以通过构造函数模拟类,是可以继承的)
- 多态:同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。换句话说,给不同的对象发送同一个消息时候,这些对象会根据这个消息分别给出不同的反馈。
2. 创建类的方式
-
工厂模式
function createObject(name, age) { var obj = new Object(); obj.name = name; obj.age = age; obj.sayHi = function () { console.log("sayHi"); }; return obj; } var per1 = createObject("Lily", 22);
-
自定义构造函数
function Person(name, age) { this.name = name; this.age = age; this.sayHi = function () { console.log("Hi"); }; } var per1 = new Person("Lily", 22);
3. 实例对象和构造函数的关系
-
实例对象的
.constructor
指向构造函数console.log(per1.constructor==Person); //true
-
对象会记住它的原型
“对象的原型”就JavaScript来讲,其实不能说对象有原型,实际上指的是对象的构造器(构造函数)的原型。而“对象将请求委托给它自己的原型”是指,对象把请求委托给它的构造器(构造函数)的原型。
console.log(per.constructor==Person); //true console.log(per.__proto__.constructor==Person); //true console.log(per.__proto__.constructor==Person.prototype.constructor); //true
-
实例对象可以访问它原型中的每一个属性和方法。
实例对象使用的属性或者方法,先在当前实例中查找,找到了则直接使用;找不到则,去实例对象的
__proto__
指向的原型(prototype
)中找,找到了则使用,找不到则报错。可以使用
hasOwnProperty()
来判断对象是否包含特定的实例成员。可以使用in来确定一个对象是否包含特定的属性。var student = { name:"Tom", age: 18 } console.log(student.hasOwnProperty("name") //true console.log(student.hasOwnProperty("toString") //false console.log("name" in student ) //true console.log("toString" in student ) //true
对象在原型链中的位置越深,找到它也就越慢,也就意味着越耗费性能。
-
如何判断对象是不是这个数据类型?
-
通过构造器的方式 实例对象.构造器==构造函数名字 (有可能会指向Object,所以尽量用第二种)
-
对象 instanceof 构造函数名字
console.log(per instanceof Person); //true console.log(per instanceof Object); //true
-
二、原型
1. 原型的概念
原型(prototype)可以理解为模板?原型的作用:一、数据共享,节省空间;二、实现继承。
2. 简单的原型实现
function Student(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
Student.prototype = {
// 相当于赋值一个{}空对象,constructor属性被覆盖了,需要手动修正constructor的指向
constructor:Student,
height: "188",
weight: "55kg",
study: function () {
console.log("I'm reading");
},
eat: function () {
console.log("I'm eating");
}
};
3. 原型链
-
用来描述实例对象和原型对象之间的关系,通过
__proto__
记录,并且指向实例对象.prototype
。 -
实例对象使用的属性或者方法,先在当前实例中查找,找到了则直接使用;找不到则,去实例对象的
__proto__
指向的原型(prototype
)中找,找到了则使用,找不到则报错。 -
实例对象不能直接改变原型对象中的属性和方法,当实例对象和原型对象的属性和方法重名时,会优先读取实例中的属性和方法。
-
原型的指向时可以改变的。
var A = funtion() {}; A.prototype = {name:"Lily"}; var B = funtion() {}; B.prototype = new A(); var b = new B(); console.log(b.name); //Lily
-
原型链不是无限长的,读取一个不存在属性
b.age
时,会遍历原型链会一层一层地网上搜索,一直到达Object.prototype
,但Object.prototype
的值为null
,因此这个属性会返回undefined
。
三、继承
1. 概念
- 继承是类与类的关系
- 继承主要通过原型链实现,继承有多种方式:原型链继承、借用构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承。其中最常用的是组合继承,最有效的是寄生组合继承。因此这里着重讨论这两种。
2. 继承的实现
2.1 原型继承
function Person(name,age,sex,weight) {
this.name=name;
this.age=age;
this.sex=sex;
this.weight=weight;
}
Person.prototype.sayHi=function () {
console.log("Hi");
};
function Student(score) {
this.score=score;
}
Student.prototype=new Person("Xiaoming",18,"male","50kg");
var stu1=new Student("100");
console.log(stu1.name,stu1.age,stu1.sex,stu1.weight,stu1.score);
stu1.sayHi();
// Xiaoming 18 male 50kg 100
var stu2=new Student("120");
console.log(stu2.name,stu2.age,stu2.sex,stu2.weight,stu2.score);
stu2.sayHi();
// Xiaoming 18 male 50kg 120
缺陷: 实现了数据共享,但new的时候直接初始化了属性,继承过来的属性的值都是相同的,只能重新调用对象的属性进行重新赋值。
2.2 组合继承(原型+构造函数)
function Person(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
Person.prototype.sayHi = function () {
console.log("Hello");
};
function Student(name,age,sex,score) {
//借用父级的构造函数传参数,this的指向是实例对象
Person.call(this,name,age,sex); //【第一次调用构造函数】
this.score = score;
}
//原型继承
Student.prototype = new Person(); //已经传值了,这里不传值 //【第二次调用构造函数】
Student.prototype.eat = function () {
console.log("eating");
};
var stu1 = new Student("Xiaoming",18,"male","100");
console.log(stu1.name, stu1.age, stu1.sex, stu1.score);
//Xiaoming 18 male 100
var stu2 = new Student("Xiaohong",16,"female","120");
console.log(stu2.name, stu2.age, stu2.sex, stu2.score);
//Xiaohong 16 female 120
缺陷:子类需要调用两次超类构造函数。会创建多余的属性。
上述代码中,实例stu2
和Student
的原型中都分别有一组name age sex
的属性。这就是调用两次Student
构造函数的结果。
2.3 寄生继承
-
概念:寄生式继承是于原型式继承紧密相关的一种思路。寄生式基础的思路与寄生构造函数和工厂模式类似,既创建一个仅用于封装继承过程的函数,该函数内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
function object(o){ function F(){} F.prototype=o; return new F(); }; // 相当于Object.create(o) function createAnother(original){ var clone = object(original); // 通过调用函数创建一个新对象 clone.sayHi = function(){ //以某种方式增强真个对象 alert("hi"); } return clone; //返回这个对象 } var person = { name:"Nicholas", friends:["Shelby","Court","Van"] } var now = createAnother(person); now.sayHi(); // hi
缺陷:使用寄生式继承方式来为对象添加函数,由于不能达到函数复用,导致效率变低,这与构造函数模式类似。
2.4 寄生组合继承
通过借用构造函数来继承属性,通过原型链来继承方法,解决了组合继承的问题。是实现基于类型继承的最有效的方式。
function inheritPrototype(subType, superType){
var protoType = Object.create(superType.prototype); //以superType的原型为模板创建对象
protoType.constructor = subType; //增强对象 constructor原本指向superType
subType.prototype = protoType; //指定对象 绑定到subType原型
}
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
}
function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function(){
console.log(this.age);
}
var instance = new SubType("Bob", 18);
instance.sayName();
instance.sayAge();
3. ES6中的继承
3.1 Class 的使用
基本上,ES6 的class
可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class
写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHi() {
console.log("Hi");
}
}
var per1 = new Person("Lily", 22);
//等同于ES5写法
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function () {
console.log("Hi");
};
var per1 = new Person("Lily", 22);
将这两种写法分别创建两个类Person
和``Person1,再分别new一个实例出来,会发现它们的
proto(也就是
prototype`)的结构是一致的。
ES6 的类,完全可以看作构造函数的另一种写法。构造函数的prototype
属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype
属性上面。
class Person {
constructor() {
// ...
}
sayHi() {
// ...
}
tostring() {
// ...
}
}
// 等同于
Person.prototype = {
constructor() {},
sayHi() {},
tostring() {},
};
3.2 Class继承
Class 可以通过extends
关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHi() {
console.log("Hi");
}
tostring() {
return `my name is ${this.name}, my age is ${this.age} `;
}
}
class Student extends Person {
constructor(name, age, score) {
super(name, age); // 调用父类的constructor(name, age)
this.score = score;
}
tostring() {
return `${super.tostring()}, my score is ${this.score}`;// 调用父类的tostring()
}
}
let stu1 = new Student("Lily", 22, 100);
stu1.sayHi(); // Hi
console.log(stu1.tostring()); //my name is Lily, my age is 22 , my score is 100
需要注意的是,子类必须在constructor
方法中调用super
方法,否则新建实例时会报错。这是因为子类自己的this
对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super
方法,子类就得不到this
对象。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
class Student extends Person {
constructor(name, age, score) {
this.score = score; // ReferenceError
super(name,agey);
this.score = score; // 正确
}
}
上面代码中,子类的constructor
方法没有调用super
之前,就使用this
关键字,结果报错,而放在super
方法之后就是正确的。