面向对象把构成问题的 transaction 分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事物在解决整个问题的过程中所发生的行为,意在写出通用代码,加强代码重用,屏蔽差异性。
一、什么是面向对象编程
js 是基于原型
的,基于面向对象编程
面向对象就是把数据和对数据的操作方法放在一起,作为一个整体——对象。对同类对象抽象出其共性,形成类
1.面向过程程序设计
将一个项目(或者一个事件)从头到尾按顺序,一步一步完成,先做什么,后做什么,一直到结束,也是我们人做事的方法。
自上而下,先确定一个整体的框架,然后添砖加瓦,逐步实现想要得到的效果,适用于简单的系统,容易理解。但是难以应对复杂的系统,不易维护扩展,难以复用
面向过程是分析解决问题的步骤,然后用函数把这些步骤一步一步的实现,然后在使用的时候一一调用则可。强调的是完成这件事儿的动作,更接近我们日常处理事情的思维。
2.面向对象程序设计
将一个项目(或者一个事件)分成更小的项目,每一个部分负责一方面的功能,最后由这些部分组成一个整体,先设计组件,在完成拼装,适用于大型复杂的系统
面向对象把构成问题的 transaction 分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事物在解决整个问题的过程中所发生的行为,意在写出通用代码,加强代码重用,屏蔽差异性。
想要弄明白面向对象,需要先理解类和对象的概念
二、创建对象的方法
1.创建字面量和实例
window.onload = function() {
// 实例
var person = new Object();
person.name = '小明';
person.age = 22;
person.year = function() {
console.log(this.name + '今年' + this.age + '岁了!')
};
person.year();
// 字面量
var student = {
name: '小明',
age: 22,
year: function () {
console.log(this.name + '今年' + this.age + '岁了!')
}
}
student.year();
}
// 小明今年22岁了!
两者输出的结果是一样的,控制台输出:
缺点:重复实例化对象,代码冗余高
2.工厂模式
window.onload = function() {
function createObj(name, age) {
var obj = new Object();
obj.name = name,
obj.age = age,
obj.year = function() {
console.log(this.name + '今年' + this.age + '岁了!')
}
return obj;
}
var obj = createObj('小明', 22);
obj.year();
}
// 小明今年22岁了!
优点:解决重复实例化对象的问题 缺点:无法识别对象的类型,因为所有的实例都指向一个原型
3.构造函数
window.onload = function() {
function Person(name, age) {
this.name = name;
this.age = age;
this.year = function() {
console.log(this.name + '今年' + this.age + '岁了!')
}
}
var student = new Person('小明', 22);
student.year();
}
// 小明今年22岁了!
优点:可以识别对象的类型 缺点:多个实例重复创建方法,无法共享
4. 原型模式
window.onload = function() {
function Par() {}
Par.prototype = {
constructor: 'Par',
name: '小明',
age: 22,
year: function() {
console.log(this.name + '今年' + this.age + '岁了!')
}
};
var son = new Par();
son.year();
}
// 小明今年22岁了!
缺点:所有实例共享他的属性和方法,不能传参和初始化属性值
5.混合模式 (推荐使用)
是构造函数和原型模式混合的写法,拥有各自的优点,构造函数共享实例属性,原型模式共享方法和想要共享的属性,可以传参和初始化属性值
先用构造函数定义对象的属性方法,然后用原型模式创建方法,使用的属性通过 prototype 获取,有一个 constructor 属性,可以指向要操作的函数对象(构造函数)
比如constructor: Par
,就代表下面这个原型方法指向Par()
对象(构造函数)
window.onload = function() {
function Par(name, age) {
this.name = name;
this.age = age;
}
Par.prototype = {
constructor: Par,
year: function() {
console.log(this.name + '今年' + this.age + '岁了!');
}
};
var son = new Par('小明', 22)
son.year();
}
// 小明今年22岁了!
三、原型,原型链
1.原型对象
- 函数对象都具有
prototype
属性,它指向函数的原型对象 (浏览器内存创建的对象),原型对象都具有constructor
属性,它指向prototype
属性所在的函数对象 (构造函数)
window.onload = function() {
function Par(name, age) {
this.name = name;
this.age = age;
}
Par.prototype = {
// constructor指向对象
constructor: Par,
year: function() {
console.log(this.name + '今年' + this.age + '岁了!');
}
};
var son = new Par('小明', 22)
son.year();
/*********************************************/
console.log(Par.prototype)
console.log(Par.prototype.constructor)
/*********************************************/
}
通过控制台可以看到
构造函数的prototypr
属性指向原型对象
原型对象的construcyor
属性指向构造函数
- 当调用构造函数创建一个实例后,该实例会有一个隐藏属性
__proto__
,它指向构造函数的原型对象
console.log(son.__proto__ === Par.prototype)
// true
- 所有的构造函数的 prototype 都是 object 类型
console.log(typeof Par.prototype)
// object
- Function 的 prototype 是一个空函数,所有内置函数的__proto__属性都指向这个空函数
console.log(Math.__proto__)
- 如果构造函数实例和原型对象中同时定义了一个属性,在调用时,会屏蔽原型对象中的属性,如果想要访问原型对象中的属性值,需要通过
delete
方法将同名属性在实例(构造函数)中彻底删除
window.onload = function () {
function Par(name) {
this.name = name;
}
Par.prototype.name = "张三";
var son = new Par("李四");
console.log(son.name); // 李四
console.log(son.__proto__.name); // 张三
// 使用 delete 删除实例的同名属性值
console.log(delete son.name); // true
console.log(son.name); // 张三
}
- 通过
hasOwnProperty(属性名)
可以判断一个属性存在于构造函数中,还是原型对象中
true
表示存在构造函数中;false
表示存在原型对象中
console.log(Par.hasOwnProperty(name)); // false
- 操作符
in
,可以判断一个属性是否存在(存在于构造函数和原型对象中皆可)
window.onload = function () {
function Par(name, age) {
this.name = name;
this.age = age;
}
Par.prototype = {
constructor: Par,
year: function() {
console.log(this.name + this.age)
}
};
var son = new Par('xm', '22')
son.year();
console.log('name' in Par); // true
console.log('age' in Par); // false
}
同样的两个属性,判断其是否存在于实例或者原型对象中,输出的结果不一样
参考:《对象中是否有某一个属性 in》https://www.cnblogs.com/IwishIcould/p/12333739.html
2.__proto__和 prototype 的区别
-
prototype
属性只有函数对象上才有,而__proto__
属性所有对象都有 -
prototype
是由函数对象指向原型对象,而__proto__
是由实例指向函数对象的原型对象 -
原型链,将父类型的实例作为子类型的原型对象,这种链式关系叫做
原型链
3.继承
- 原型链继承
优点:父类原型定义的属性和方法可以复用 缺点:子类实例没有自己的属性,不能向父类传递参数
function test1() {
function SuperType() {
this.city = [ "北京", "上海", "天津" ];
this.property = true;
}
SuperType.prototype = {
constructor : SuperType, // 保持构造函数和原型对象的完整性
age : 15,
getSuperValue : function() {
return this.property;
}
};
function SonType() {
this.property = false;
}
// 重写子类的原型指向父类的实例:继承父类的原型
SubType.prototype = new SuperType();
SubType.prototype = {
constructor : SubType,
getSonType : function() {
return this.property;
}
};
// 优点验证
let son = new SubType();
console.log(son.age); // 15
console.log(son.getSuperValue()); // false
// 缺点验证
let instance1 = new SubType();
instance1.city.push("重庆");
console.log(instance1.city); // ["北京", "上海", "天津", "重庆"]
let instance2 = new SubType();
console.log(instance2.city); // ["北京", "上海", "天津", "重庆"]
}
// test1();
- 构造函数继承
优点:子类实例有自己的属性,可以向父类传递参数,解决原型链继承的缺点 缺点:父类原型的属性和方法不可复用
function test2() {
function SuperType(name) {
this.name = name;
this.city = [ "北京", "上海", "天津" ]
}
SuperType.prototype = {
constructor : SuperType,
age : 18,
showInfo : function() {
return this.name;
}
};
function SubType() {
// 父类调用 call() 或者 apply() 方法和子类共用同一个 this,实现子类实例属性的继承
SuperType.call(this, "张三");
}
// 优点验证
let instance = new SubType();
instance.city.push("重庆");
console.log(instance.city); // ["北京", "上海", "天津", "重庆"]
let instance1 = new SubType();
console.log(instance1.city); // ["北京", "上海", "天津"]
// 缺点验证
console.log(instance.age); // undefined
instance.showInfo(); // son.showInfo is not a function
}
// test2();
- 组合继承(推荐)
优点:原型的属性和方法可以复用,每个子类实例都有自己的属性 缺点:父类构造函数调用了两次,子类原型中的父类实例属性被子类实例覆盖
function test3() {
function SuperType(name) {
this.name = name;
this.city = [ "北京", "上海", "天津" ]
}
SuperType.prototype = {
constructor : SuperType,
showInfo : function() {
console.log(this.name + "今年" + this.age + "岁了");
}
};
function SubType(name, age) {
// 1. 通过构造方法继承实现实例属性的继承
SuperType.call(this, name);
this.age = age;
}
// 2. 通过原型链继承实现原型方法的继承
SubType.prototype = new SuperType();
// 优点验证
let instance = new SubType("张三", 15);
instance.showInfo(); // 张三今年 15 岁了
let instance1 = new SubType();
instance1.city.push("重庆");
console.log(instance1.city); // ["北京", "上海", "天津", "重庆"]
let instance2 = new SubType();
console.log(instance2.city); // ["北京", "上海", "天津"]
}
// test3();
- 寄生组合继承(推荐)
优点:解决了组合继承的缺点,效率高 缺点:基本没有
function test4() {
function inheritPrototype(subType, superType) {
// 1. 继承父类的原型
var prototype = Object.create(superType.prototype);
// 2. 重写被污染的 construct
prototype.constructor = subType;
// 3. 重写子类的原型
subType.prototype = prototype;
}
function SuperType(name) {
this.name = name;
this.city = [ "北京", "上海", "天津" ];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
// 将父类原型指向子类
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
console.log(this.age);
}
// 优点验证
let instance = new SubType("张三", 15);
instance.sayName(); // 张三
let instance1 = new SubType();
instance1.city.push("重庆");
console.log(instance1.city); // ["北京", "上海", "天津", "重庆"]
let instance2 = new SubType();
console.log(instance2.city); // ["北京", "上海", "天津"]
}
// test4();
4.ES6 新方法--class
新的关键字class
在 es6 开始被引入到 javascript 中来,class
的目的就是让定义类更简单
用函数方法实现:
function Person(name) {
this.name = name;
}
Person.prototype.hello = function () {
console.log('Hello, ' + this.name + '!');
}
var son = new Person('xm')
son.hello(); // Hello, xm!
用class
来实现:
class Person {
constructor(name) {
this.name = name;
}
hello() {
console.log('Hello, ' + this.name + '!');
}
}
var son = new person('xm')
son.hello(); // Hello, xm!
可以在看到,在定义class
中,直接包含了构造函数constructor
属性,和原型对象上的函数hello()
方法,省略掉了function
关键字
需要注意:原来的写法是,构造函数和原型对象分散开来写,现在用class
可以直接把两者串在一个对象中,只有最后传参和调用方法时写法是一样的
class 继承
用class
定义对象的另一个巨大的好处是继承更方便了。想一想我们从Person
派生一个PrimaryPerson
需要编写的代码量。现在,原型继承的中间对象,原型对象的构造函数等等都不需要考虑了,直接通过extends
来实现:
class PrimaryPerson extends Person {
constructor(name, grade) {
super(name); // 记得用 super 调用父类的构造方法!
this.grade = grade;
}
myGrade() {
alert('I am at grade ' + this.grade);
}
}
注意PrimaryPerson
的定义也是通过 class 关键字实现的,而extends
则表示原型链对象来自Person
,子类的构造函数可能会和父类的不太相同
例如,PrimaryPerson
需要name
和grade
两个参数,并且需要通过super(name)
来调用父类的构造函数,否则父类的name
属性无法正常初始化。
PrimaryPerson
已经自动获得了父类Person
的hello方法
,我们又在子类中定义了新的myGrade
方法。
ES6 引入的class
和原有的JavaScript原型继承
有什么区别呢?
实际上它们没有任何区别,class
的作用就是让 JavaScript 引擎去实现原来需要我们自己编写的原型链代码。简而言之,用class
的好处就是极大地简化了原型链代码。
但是!
目前并不是所有的浏览器都支持class
,所以在选择的时候一定要慎重!