面向对象
1. 面向对象概述
面向对象程序设计,英文全称 Object Oriented Programming,简称 OOP,这里的 Object,中文翻译成了对象,实际上就是指的物体。
2. 面向对象的特征
面向对象具备三大特征:分别是封装、继承、多态,下面将对这三个特点进行一个简单的介绍。
**封装:**在 OOP 编程中,把所有编程逻辑放在一个对象中,对外提供可用的功能,但是外部并不需要知道这个功能是如何实现的。
**继承:**在 OOP 编程中,我们可以继承已存在的对象的所有属性和方法,然后可以通过添加新的属性和方法来改进其功能。
**多态:**在 OOP 编程中,不同的对象可以共享相同的方法,还能用一些特定的方法来覆盖原有的方法。
3. 描述对象
在程序中,我们描述一个对象可以通过以下两个方面来进行描述。
**对象特征:**对象特征又被称为成员属性,一般通过变量来进行存储。
**对象功能:**对象功能在程序里一般是通过方法(函数)来进行描述的。
4. 类
类与对象的关系可以总结成一句话:类是对象的一种概括,而对象是类的一种具体实现。
原型对象
1. 原型对象
在 JavaScript 中,每一个对象都有一个原型对象。而原型对象上面也有一个自己的原型对象,一层一层向上找,最终会到达 null。如下图所示
由上图可得一下结论:
- JavaScript 中每个对象都有一个原型对象,可以通过 proto 属性来访问到对象的原型对象。
- 通过 proto 属性一直向上寻找原型对象的话,最终会找到 null。
- 构造函数的 prototype 属性指向一个对象,这个对象是该构造函数实例化出来的原型对象。
- JavaScript 中的根对象是 Object.prototype 对象。Object.prototype 对象是一个空对象。
- JavaScript 中的每一个对象,都是从 Object.prototype 对象克隆而来的。Object.prototype 对象就是它们的原型Object.prototype对象的原型为 null。
2. 对象分类
在 ECMAScript 6 之前,对象可以分为两大类,分别是原生对象和宿主对象。
原生对象
原生对象又可以分为两类:内置对象和自定义对象。
像Date、Math、Array 等,这些就是典型的内置对象,它们是 JavaScript 这门语言本身所内置的,直接使用即可。
自定义对象则是开发人员自己定义的对象。
宿主对象
顾名思义,就是依存于某一个特定的环境才会有的对象。一旦离开了特定的环境,则这些对象将不存在。
在 ECMAScript 6 中,对象的类别得到了扩充,分为了四个类别,分别是普通对象、外来对象、标准对象和内置对象。
原型相关方法
1. prototype 和__proto__
prorotype 是构造函数上面的一个属性,指向一个对象,这个对象是该构造函数实例化出来的对象的原型对象。实例化出来的对象可以通过__proto__来找到自己原型对象。如下:
const arr = [1,2,3,4,5];
console.log(Array.prototype); // []
console.log(arr.__proto__); // []
console.log(Array.prototype === arr.__proto__); // true
2.Object.getPrototypeOf() 方法
Object.getPrototypeOf() 方法也能查找一个对象的原型对象。如下:
let arr = [1,2,3,4,5]
console.log(Object.getPrototypeOf(arr)); // []
console.log(arr.__proto__); // []
console.log(Object.getPrototypeOf(arr) === arr.__proto__); // true
3. constructor 属性
constructor 属性可以查看到一个对象的构造函数是什么。如下:
const arr = [1,2,3,4,5];
console.log(arr.constructor); // [Function: Array]
4. instanceof 操作符
判断一个对象是否是一个构造函数的实例。如下:
const arr = [1,2,3,4,5];
console.log(arr instanceof Array); // true
console.log(arr instanceof Number); // false
5. hasOwnProperty() 方法
判断一个属性是定义在对象本身上面还是从原型对象上面继承而来的。如下:
const person = {
arms: 2,
legs: 2,
}
const yu = Object.create(person, {
name: {
value: "yu",
writable: false,
enumerable: true
},
age: {
value: 18,
enumerable: false
}
});
console.log(yu.hasOwnProperty("name")); // true
console.log(yu.hasOwnProperty("age")); // true
console.log(yu.hasOwnProperty("arms")); // false
console.log(yu.hasOwnProperty("legs")); // false
构造函数
1. 构造函数创建对象
JavaScript 能够模拟出面向对象的编程风格,使用函数来模拟其他面向对象语言中的类。用于实例化对象的函数,我们将其称之为构造函数。不过,构造函数的是指也就是一个函数而已。所以,为了区分普通函数和构造函数,有一个不成文的规定,那就是构造函数的函数名首字母大写。
如何书写一个构造函数如下:
const Computer = function(name,price){
this.name = name;
this.price = price;
}
Computer.prototype.showSth = function(){
console.log(`这是一台${this.name}电脑`);
}
通过 new 运算符,就可以从构造函数中实例化出来一个对象,通常情况下,构造函数里面的 this 就指向返回的这个对象。如下:
const Computer = function (name, price) {
this.name = name;
this.price = price;
}
Computer.prototype.showSth = function () {
console.log(this); // 打印出 this 所指向的对象
console.log(`这是一台${this.name}电脑`);
}
const apple = new Computer("苹果", 12000);
console.log(apple.name);// 苹果
console.log(apple.price);// 12000
apple.showSth();// Computer { name: '苹果', price: 12000 } 这是一台苹果电脑
const asus = new Computer("华硕", 5000);
console.log(asus.name);// 华硕
console.log(asus.price);// 5000
asus.showSth();// Computer { name: '华硕', price: 5000 } 这是一台华硕电脑
注:当我们使用 new 运算符从构造函数实例化对象时,实际上在 JavaScript 引擎内部,也是先克隆了 Object.prototype 对象,然后再进行一些其他的额外操作。
2. 构造函数显式返回内容
再构造函数中可以通过 return 关键字返回数据,不过,需要注意的是,如果构造函数显式地返回了一个 object 类型的对象,那么此次运算结果最终会是返回这个对象,而不是 this。
正常情况,,构造函数没有返回 object 类型对象,this 指向实例化出来对象
const Computer = function (name, price) {
this.name = name;
this.price = price;
}
Computer.prototype.showSth = function () {
console.log(this); // 打印出 this 所指向的对象
}
const apple = new Computer("苹果", 12000);
console.log(apple.name); // 苹果
apple.showSth(); // Computer { name: '苹果', price: 12000 }
如果构造函数显式的返回一个 object 类型的对象,那么最终是同的就是手动返回的这个对象
const Computer = function (name, price) {
this.name = name;
this.price = price;
// 显示返回一个 object 类型对象
return {
name: "yu",
showSth: function () {
console.log(this); // 打印出 this 所指向的对象
}
}
}
Computer.prototype.showSth = function () {
console.log(this); // 打印出 this 所指向的对象
}
const apple = new Computer("苹果", 12000);
console.log(apple.name); // yu
apple.showSth(); // { name: 'yu', showSth: [Function: showSth] }
从上面的例子可以看出,如果构造器函数不显式返回任何数据,this 就指向实例化出来的对象,而如果显式的返回一个 object 类型的对象,那么最终是同的就是手动返回的那个对象。那么为什么一直强调是 object 类型对象呢?
事实上,如果返回的只是普通数据,那么 this 也是指向实例化出来的对象。如下:
const Student = function () {
this.name = 'yu';
return {
name: 'song'
};
}
const obj = new Student();
console.log(obj.name); // song
返回普通数据的情况:
const Student = function () {
this.name = 'yu';
return 'song';
}
const obj = new Student();
console.log(obj.name); // yu
3. ECMAScript 6 中类的声明
从 ECMAScript 6 开始,JavaScript 已经开始越来越贴近其他的高级语言了。在 ECMAScript 6 中有了类这个概念,使用 class 关键字来声明一个类,然后从类里面实例化对象。
注:虽然有了 class 关键字,但这只是一个语法糖,语法背后对象的创建,还是使用的是原型的方式。
具体实例如下:
class Computer {
// 构造器
constructor(name, price) {
this.name = name;
this.price = price;
}
// 原型方法
showSth() {
console.log(`这是一台${this.name}电脑`);
}
}
const apple = new Computer("苹果", 12000);
console.log(apple.name); // 苹果
console.log(apple.price); // 12000
apple.showSth(); // 这是一台苹果电脑
4. 静态方法
静态方法又被称为类方法,顾名思义,就是通过类来调用的方法。静态方法的好处在于不需要实例化对象,直接通过类就能够进行方法调用。
在 ECMAScript 6 中要创建静态方法,可以在方法前面条件关键字 static,如下:
class Computer {
// 构造器
constructor(name, price) {
this.name = name;
this.price = price;
}
// 原型方法
showSth() {
console.log(`这是一台${this.name}电脑`);
}
// 静态方法
static comStruct() {
console.log("电脑由显示器、主机、键鼠组成");
}
}
Computer.comStruct(); // 电脑由显示器、主机、键鼠组成
如果书写的是构造函数,也有办法来模拟静态方法,直接将方法挂在构造函数上即可,如下:
const Computer = function (name, price) {
this.name = name;
this.price = price;
}
Computer.prototype.showSth = function () {
console.log(`这是一台${this.name}电脑`);
}
// 静态方法 直接通过 Computer 这个构造函数来调用
Computer.comStruct = function () {
console.log("电脑由显示器、主机、键鼠组成");
}
Computer.comStruct(); // 电脑由显示器、主机、键鼠组成
面向对象三大特征
1. 封装
封装指隐藏内部细节,不暴露在外面
JavaScript 中想要实现封装的方法很简单,只需要在声明属性的时候,添加关键字(let、const、var)即可。如下:
const Computer = function (name, price) {
this.name = name;
const _price = price;
}
Computer.prototype.showSth = function () {
console.log(`这是一台${this.name}电脑`);
}
const apple = new Computer("苹果", 12000);
console.log(apple.name); // 苹果
console.log(apple.price); // undefined
console.log(apple._price); // undefined
封装后的属性,我们称其为私有属性,对外部而言,私有属性是不可见的。
2. 继承
继承基本介绍
面向对象里面的继承是指一个子类去继承一个父类。子类继承了父类之后,父类所有的属性和方法都自动拥有了。
继承的好处
继承最大的好处,就在于代码复用
继承的缺点
如果继承设计得非常复杂,那么整个程序的设计也会变得庞大和臃肿。甚至会出现”大猩猩与香蕉“的问题
”大猩猩与香蕉“这个名字来自于 Erlang 编程语言的创始人 JoeArmstrong 的文章:你想要一个香蕉,但是得到的是整个丛林里拿着大香蕉的大猩猩
对象冒充
所谓对象冒充,就是用父类(构造函数)去充当子类的属性,如下:
const Person = function (name, age) {
this.name = name;
this.age = age;
}
const Student = function (name, age, gender, score) {
this.temp = Person; // 将 Person 构造函数作为 Student 的一个属性
this.temp(name, age); // 给 Person 构造函数里面的 this.name 以及 this.age 赋值
delete this.temp; // this.temp 已经无用,将其删除
this.gender = gender;
this.score = score;
}
const bainiao = new Student("白鸟", 18, "男", 100);
console.log(bainiao.name); // 白鸟
console.log(bainiao.age); // 18
console.log(bainiao.gender); // 男
console.log(bainiao.score); // 100
注:对象冒充来实现继承有一个缺陷,无法继承到原型对象上面的属性
方法借用
方法借用,在 JavaScript 中非常灵活,不需要继承就可以从父类或者原型上面借用相应的属性和方法。
cell() 和 apply()这两个方法本质上作用是一样的,就是一个方法,区别在于参数的接收方式不同
这两个方法是在Function.prototype.cell()
、Function.prototype.apply()
A.cell(B,参数)
A 代表一个方法,B 代表 this 指向,将 A 这个方法应用到 B 上面(相当于 B 借用 A 方法)
Function.prototype.cell(this,参数1,参数2,参数3…);
Function.prototype.cell(this,[]参数1,参数2,参数3…]);
原型继承
这种方式的核心思路就是改变函数的 prototype 的指向,使其指定到我们想要继承的类的实例对象上面。如下:
const Person = function (name, age) {
this.name = name;
this.age = age;
}
Person.prototype.test = "this is a test";
const Student = function (name, age, gender, score) {
// 将 Person 构造函数应用到 this 里面
// this 后面是参数
Person.apply(this, [name, age]);
this.gender = gender;
this.score = score;
}
Student.prototype = new Person(); // 改变 Student 构造函数的原型对象
const bainiao = new Student("白鸟", 18, "男", 100);
console.log(bainiao.name); // 白鸟
console.log(bainiao.age); // 18
console.log(bainiao.gender); // 男
console.log(bainiao.score); // 100
console.log(bainiao.test); // this is a test
** ECMAScript 6 继承方式**
从 ECMAScript 6 开始,可以使用 extends 关键字来实现继承了。如下:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayName() {
console.log(`my name is ${this.name}`);
}
}
class Student extends Person {
constructor(name, age, gender, score) {
super(name, age); // super 代表访问父类的构造函数
this.gender = gender;
this.score = score;
}
learn() {
console.log("I\'m learning");
}
}
const bainiao = new Student("白鸟", 18, "male", 100);
console.log(bainiao.name); // 白鸟
console.log(bainiao.age); // 18
console.log(bainiao.gender); // male
console.log(bainiao.score); // 100
bainiao.sayName(); // my name is 白鸟
bainiao.learn(); // I'm learning
3. 多态
多态简介
同一操作作用域不同的对象上面,可以产生不同的解释和不同的执行结果
最常见的多态,就是 toString()
方法
const i = 3;
console.log(i.toString()); // "3"
const j = [1, 2, 3];
console.log(j.toString()); // "1,2,3"
多态的意义
Martin Fowler 在《重构:改善既有代码的设计》里写道:
多态最根本的好处在于,你不必再向对象询问“你是什么类型”
够根据得到的答案调用对象的某个行为,你只需要用该行为就是了,其他的一切多态机制都会为你安排妥当。
将行为分布在各个对象中,并让这些对象各自负责自己的行为,这正是面向对象设计的优点。
this指向
- 以普通函数的方式被调用
这个时候this指向全局对象(浏览器:window,Node:global)
function test(){
console.log(this)
}
test()//global
- 函数以对象的方法的形式被调用
这个时候this指向当前对象
let obj={
name : 'ljy',
say:function(){
console.log(this);
}
}
obj.say()//obj
- 箭头函数的 this 指向
在声明的时候就已经决定了,this和外层的作用域相同
let obj={
name : 'ljy',
say : ()=>{
console.log(this);
}
}
obj.say()//{}