JavaScript 进阶教程(1)--面向对象编程
目录
1 学习目标
2 面向对象介绍
2.1 什么是对象
2.2 什么是面向对象
2.3 JavaScript 中面向对象的基本体现
3 JavaScript 如何创建对象
3.1 字面量方式
3.2 简单方式的改进:工厂函数
3.3 更优雅的工厂函数:构造函数
3.4 构造函数代码执行过程
3.5 构造函数和实例对象的关系
3.6 构造函数的问题
4 原型
4.1 更好的解决方案: prototype
4.2 属性成员的搜索原则:原型链
4.3 实例对象读写原型对象成员
4.4 更简单的原型语法
4.5 原生对象的原型
4.6 原型对象的一些问题
1 学习目标
理解面向对象开发思想
掌握 JavaScript 面向对象开发相关模式
2 面向对象介绍
2.1 什么是对象
Everything is object (一切皆对象)
我们可以从两个层次来理解对象:
(1) 对象是单个事物的抽象。
一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个与远程服务器的连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。
(2) 对象是一个容器,封装了属性(property)和方法(method)。
属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal对象,使用“属性”记录具体是那一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。
在实际开发中,对象是一个抽象的概念,可以将其简单理解为:数据集或功能集。ECMAScript-262 把对象定义为:无序属性的集合,其属性可以包含基本值、对象或者函数。 严格来讲,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都 映射到一个值。
提示:每个对象都是基于一个引用类型创建的,这些类型可以是系统内置的原生类型,也可以是开发人员自定义的类型。
2.2 什么是面向对象
面向对象不是新的东西,它只是过程式代码的一种高度封装,目的在于提高代码的开发效率和可维护性。
面向对象编程 —— Object Oriented Programming,简称 OOP ,是一种编程开发思想。 它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。 因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。
面向对象与面向过程区别:
面向过程就是亲力亲为,事无巨细,面面俱到,步步紧跟,有条不紊。
面向对象就是找一个对象,指挥得结果。
面向对象将执行者转变成指挥者。
面向对象不是面向过程的替代,而是面向过程的封装。
面向对象的特性:
封装性
继承性
多态性
扩展阅读:
知乎:什么是面向对象编程思想?
2.3 JavaScript 中面向对象的基本体现
在 JavaScript 中,所有数据类型都可以视为对象,当然也可以自定义对象。 自定义的对象数据类型就是面向对象中的类( Class )的概念。
我们以一个例子来说明面向过程和面向对象在程序流程上的不同之处。
假设我们要处理学生的成绩表,为了表示一个学生的成绩,面向过程的程序可以用一个对象表示:
var std1 = { name: '张三', score: 98 }
var std2 = { name: '李四', score: 81 }
而处理学生成绩可以通过函数实现,比如打印学生的成绩:
function printScore (student) {console.log('姓名:' + student.name + ' ' + '成绩:' + student.score)
}
如果采用面向对象的程序设计思想,我们首选思考的不是程序的执行流程, 而是 Student
这种数据类型应该被视为一个对象,这个对象拥有 name
和 score
这两个属性(Property)。 如果要打印一个学生的成绩,首先必须创建出这个学生对应的对象,然后,给对象发一个 printScore
消息,让对象自己把自己的数据打印出来。
抽象数据行为模板(Class):
function Student (name, score) {this.name = namethis.score = score
}Student.prototype.printScore = function () {console.log('姓名:' + this.name + ' ' + '成绩:' + this.score)
}
根据模板创建具体实例对象(Instance):
var std1 = new Student('张三', 98)
var std2 = new Student('李四', 81)
实例对象具有自己的具体行为(给对象发消息):
std1.printScore() // => 姓名:张三 成绩:98
std2.printScore() // => 姓名:李四 成绩 81
面向对象的设计思想是从自然界中来的,因为在自然界中,类(Class)和实例(Instance)的概念是很自然的。 Class 是一种抽象概念,比如我们定义的 Class——Student ,是指学生这个概念, 而实例(Instance)则是一个个具体的 Student ,比如, 张三 和 李四 是两个具体的 Student 。
面向对象的设计思想是:
抽象出 Class
根据 Class 创建 Instance
指挥 Instance 得结果
面向对象的抽象程度比函数要高,因为一个 Class 既包含数据,又包含操作数据的方法。
3 JavaScript 如何创建对象
3.1 字面量方式
我们可以直接通过 new Object()
创建:
var person = new Object()
person.name = '张三'
person.age = 18
person.sayName = function () {console.log(this.name)
}
每次创建通过 new Object()
比较麻烦,所以可以通过它的简写形式对象字面量来创建:
var person = {name: '张三',age: 18,sayName: function () {console.log(this.name)}
}
上面的写法是没有问题的,但是假如我们要生成两个 person
实例对象呢?
var person1 = {name: '张三',age: 18,sayName: function () {console.log(this.name)}
}
var person2 = {name: '李四',age: 16,sayName: function () {console.log(this.name)}
}
通过上面的代码我们不难看出,这样写的代码太过冗余,重复性太高。
3.2 简单方式的改进:工厂函数
我们可以写一个函数,解决上边代码重复的问题:
function createPerson (name, age) {return {name: name,age: age,sayName: function () {console.log(this.name)}}
}
生成实例对象:
var p1 = createPerson('张三', 18)
var p2 = createPerson('李四', 18)
这样封装比上边的方式好多了,通过工厂模式我们解决了创建多个相似对象代码冗余的问题, 但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
3.3 更优雅的工厂函数:构造函数
一种更优雅的工厂函数就是下面这样,构造函数:
function Person (name, age) {this.name = namethis.age = agethis.sayName = function () {console.log(this.name)}
}
var p1 = new Person('张三', 18)
p1.sayName() // => 张三
var p2 = new Person('李四', 23)
p2.sayName() // => 李四
在上面的示例中,Person()
函数取代了 createPerson()
函数,但是实现效果是一样的。 这是为什么呢?
我们注意到,Person()
中的代码与 createPerson()
有以下几点不同之处:
没有显示的创建对象
直接将属性和方法赋给了
this
对象没有
return
语句函数名使用的是大写的
Person
3.4 构造函数代码执行过程
要创建 Person
实例,则必须使用 new
操作符。 以这种方式调用构造函数会经历以下 4 个步骤:
创建一个新对象。
将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)。
执行构造函数中的代码。
返回新对象。
下面是具体的伪代码:
function Person (name, age) {// 当使用 new 操作符调用 Person() 的时候,实际上这里会先创建一个对象// var instance = {}// 然后让内部的 this 指向 instance 对象// this = instance// 接下来所有针对 this 的操作实际上操作的就是 instance
this.name = namethis.age = agethis.sayName = function () {console.log(this.name)}
// 在函数的结尾处会将 this 返回,也就是 instance// return this
}
3.5 构造函数和实例对象的关系
使用构造函数的好处不仅仅在于代码的简洁性,更重要的是我们可以识别对象的具体类型了。 在每一个实例对象中的_proto_中同时有一个 constructor
属性,该属性指向创建该实例的构造函数:
console.log(p1.constructor === Person) // => true
console.log(p2.constructor === Person) // => true
console.log(p1.constructor === p2.constructor) // => true
对象的 constructor
属性最初是用来标识对象类型的, 但是,如果要检测对象的类型,还是使用 instanceof
操作符更可靠一些:
console.log(p1 instanceof Person) // => true
console.log(p2 instanceof Person) // => true
总结:
1 构造函数是根据具体的事物抽象出来的抽象模板。
2 实例对象是根据抽象的构造函数模板得到的具体实例对象。
3 每一个实例对象都具有一个 constructor
属性,指向创建该实例的构造函数。( 此处constructor
是实例的属性的说法不严谨,具体后面的原型会讲到)
4 可以通过实例的 constructor
属性判断实例和构造函数之间的关系。(这种方式不严谨,推荐使用 instanceof
操作符,后面学原型会解释为什么)
3.6 构造函数的问题
使用构造函数带来的最大的好处就是创建对象更方便了,但是其本身也存在一个浪费内存的问题:
function Person (name, age) {this.name = namethis.age = agethis.type = '学生'this.sayHello = function () {console.log('hello ' + this.name)}
}
var p1 = new Person('王五', 18)
var p2 = new Person('李四', 16)
上边的代码,从表面看上好像没什么问题,但是实际上这样做,有一个很大的弊端。 那就是对于每一个实例对象,type
和 sayHello
都是一模一样的内容, 每一次生成一个实例,都必须为重复的内容,多占用一些内存,如果实例对象很多,会造成极大的内存浪费。
console.log(p1.sayHello === p2.sayHello) // => false
对于这种问题我们可以把需要共享的函数定义到构造函数外部:
function sayHello = function () {console.log('hello ' + this.name)
}
function Person (name, age) {this.name = namethis.age = agethis.type = '学生'this.sayHello = sayHello
}
var p1 = new Person('王五', 18)
var p2 = new Person('李四', 16)
console.log(p1.sayHello === p2.sayHello) // => true
这样确实可以了,但是如果有多个需要共享的函数的话就会造成全局命名空间冲突的问题。如何解决这个问题呢?你肯定想到了可以把多个函数放到一个对象中用来避免全局命名空间冲突的问题:
var fns = {sayHello: function () {console.log('hello ' + this.name)},sayAge: function () {console.log(this.age)}
}
function Person (name, age) {this.name = namethis.age = agethis.type = '学生'this.sayHello = fns.sayHellothis.sayAge = fns.sayAge
}
var p1 = new Person('王五', 18)
var p2 = new Person('李四', 16)
console.log(p1.sayHello === p2.sayHello) // => true
console.log(p1.sayAge === p2.sayAge) // => true
至此,我们利用自己的方式基本上解决了构造函数的内存浪费问题。 但是代码看起来还是那么的格格不入,那有没有更好的方式呢?
4 原型
4.1 更好的解决方案: prototype
Javascript 规定,每一个构造函数都有一个 prototype
属性,指向另一个对象。 这个对象的所有属性和方法,都会被构造函数的实例继承。
这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype
对象上。
function Person (name, age) {this.name = namethis.age = age
}
console.log(Person.prototype)
Person.prototype.type = '学生'
Person.prototype.sayName = function () {console.log(this.name)
}
var p1 = new Person(...)
var p2 = new Person(...)
console.log(p1.sayName === p2.sayName) // => true
这时所有实例的 type
属性和 sayName()
方法, 其实都是同一个内存地址,指向 prototype
对象,因此就提高了运行效率。
构造函数、实例、原型三者之间的关系:
任何函数都有一个 prototype
属性,该属性是一个对象。
function F () {}
console.log(F.prototype) // => object
F.prototype.sayHi = function () {console.log('hi!')
}
构造函数的 prototype
对象默认都有一个 constructor
属性,指向 prototype
对象所在函数。
console.log(F.constructor === F) // => true
通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype
对象的指针 __proto__
。
var instance = new F()
console.log(instance.__proto__ === F.prototype) // => true
`__proto__` 是非标准属性。
实例对象可以直接访问原型对象成员:
instance.sayHi() // => hi!
总结:
任何函数都具有一个
prototype
属性,该属性是一个对象。构造函数的
prototype
对象默认都有一个constructor
属性,指向prototype
对象所在函数。通过构造函数得到的实例对象内部会包含一个指向构造函数的
prototype
对象的指针__proto__。
所有实例都直接或间接继承了原型对象的成员。
4.2 属性成员的搜索原则:原型链
了解了 构造函数-实例-原型对象 三者之间的关系后,接下来我们来解释一下为什么实例对象可以访问原型对象中的成员。
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。
搜索首先从对象实例本身开始。
如果在实例中找到了具有给定名字的属性,则返回该属性的值。
如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。
如果在原型对象中找到了这个属性,则返回该属性的值。
也就是说,在我们调用 person1.sayName()
的时候,会先后执行两次搜索:
首先,解析器会问:“实例 person1 有 sayName 属性吗?”答:“没有。
然后,它继续搜索,再问:“ person1 的原型有 sayName 属性吗?”答:“有。
于是,它就读取那个保存在原型对象中的函数。
当我们调用 person2.sayName() 时,将会重现相同的搜索过程,得到相同的结果。
这就是多个对象实例共享原型所保存的属性和方法的基本原理。
总结:
先在自己身上找,找到即返回。
自己身上找不到,则沿着原型链向上查找,找到即返回。
如果一直到原型链的末端还没有找到,则返回
undefined。
4.3 实例对象读写原型对象成员
读取:
先在自己身上找,找到即返回。
自己身上找不到,则沿着原型链向上查找,找到即返回。
如果一直到原型链的末端还没有找到,则返回
undefined。
值类型成员写入(实例对象.值类型成员 = xx
):
当实例期望重写原型对象中的某个普通数据成员时实际上会把该成员添加到自己身上。
也就是说该行为实际上会屏蔽掉对原型对象成员的访问。
引用类型成员写入(实例对象.引用类型成员 = xx
):同上。
复杂类型修改(实例对象.成员.xx = xx
):
同样会先在自己身上找该成员,如果自己身上找到则直接修改。
如果自己身上找不到,则沿着原型链继续查找,如果找到则修改。
如果一直到原型链的末端还没有找到该成员,则报错(
实例对象.undefined.xx = xx
)。
4.4 更简单的原型语法
我们注意到,前面例子中每添加一个属性和方法就要敲一遍 Person.prototype
。 为减少不必要的输入,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象:
function Person (name, age) {this.name = namethis.age = age
}
Person.prototype = {type: '学生',sayHello: function () {console.log('我叫' + this.name + ',我今年' + this.age + '岁了')}
}
在该示例中,我们将 Person.prototype
重置到了一个新的对象。 这样做的好处就是为 Person.prototype
添加成员简单了,但是也会带来一个问题,那就是原型对象丢失了 constructor
成员。
所以,我们为了保持 constructor
的指向正确,建议的写法是:
function Person (name, age) {this.name = namethis.age = age
}Person.prototype = {constructor: Person, // => 手动将 constructor 指向正确的构造函数type: '学生',sayHello: function () {console.log('我叫' + this.name + ',我今年' + this.age + '岁了')}
}
4.5 原生对象的原型
所有函数都有 prototype 属性对象。
- Object.prototype
- Function.prototype
- Array.prototype
- String.prototype
- Number.prototype
- Date.prototype
- ...
为数组对象和字符串对象扩展原型方法:
//为内置对象添加原型方法
//我们在系统的对象的原型中添加方法,相当于在改变源码
//我希望字符串中有一个倒序字符串的方法
String.prototype.myReverse = function() {for (var i = this.length - 1; i >= 0; i--) {console.log(this[i]);}
};
var str = "abcdefg";
str.myReverse();//为Array内置对象的原型对象中添加方法
Array.prototype.mySort = function() {for (var i = 0; i < this.length - 1; i++) {for (var j = 0; j < this.length - 1 - i; j++) {if (this[j] < this[j + 1]) {var temp = this[j];this[j] = this[j + 1];this[j + 1] = temp;} //end if} // end for} //end for
};var arr = [100, 3, 56, 78, 23, 10];
arr.mySort();
console.log(arr);String.prototype.sayHi = function() {console.log(this + "哈哈,我又变帅了");
};//字符串就有了打招呼的方法
var str2 = "小杨";
str2.sayHi();
4.6 原型对象的一些问题
共享数组
共享对象
如果真的希望可以被实例对象之间共享和修改这些共享数据那就不是问题。但是如果不希望实例之间共享和修改这些共享数据则会出现问题。一个更好的建议是,最好不要让实例之间互相共享数组或者对象成员,一旦修改的话会导致数据的走向很不明确而且难以维护。
原型对象使用建议:
私有成员(一般就是非函数成员)放到构造函数中。
共享成员(一般就是函数)放到原型对象中。
如果重置了
prototype
记得修正constructor
的指向。
今天的学习就到这里,你可以使用今天学习的技巧来改善一下你曾经的代码,如果想继续提高,欢迎关注我,每天学习进步一点点,就是领先的开始。如果觉得本文对你有帮助的话,欢迎点赞,评论,转发!!!
JavaScript 进阶教程(1)--面向对象编程相关推荐
- JavaScript 进阶教程(2)---面向对象实战之贪吃蛇小游戏
目录 1 引言 2 游戏地图 3 游戏对象 3.1 食物对象 3.2 小蛇对象 3.3 游戏对象 4 游戏的逻辑 4.1小蛇的移动 4.2 让蛇自己动起来 4.2.1 自动移动 4.2.2 自调用函数 ...
- JavaScript强化教程-JS面向对象编程
对事物的抽象描述 描述这类事物的特征和行为 对象是类的实例 代码实现:创建一个类 function peple(){this.hp=0;this.act = 30;this.name = &q ...
- javascript进阶教程第二章对象案例实战
javascript进阶教程第二章对象案例实战 一.学习任务 通过几个案例练习回顾学过的知识 通过案例练习补充几个之前没有见到或者虽然讲过单是讲的不仔细的知识点. 二.具体实例 温馨提示 面向对象的知 ...
- javascript进阶教程第一章案例实战
javascript进阶教程第一章案例实战 一.学习任务 通过几个案例练习回顾学过的知识 通过练习积累JS的使用技巧 二.实例 练习1:删除确认提示框 实例描述: 防止用户小心单击了"删除& ...
- Scala进阶之路-面向对象编程之类的成员详解
Scala进阶之路-面向对象编程之类的成员详解 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.Scala中的object对象及apply方法 1>.scala 单例对象 ...
- Java基础教程:面向对象编程[2]
Java基础教程:面向对象编程[2] 内容大纲 访问修饰符 四种访问修饰符 Java中,可以使用访问控制符来保护对类.变量.方法和构造方法的访问.Java 支持 4 种不同的访问权限. default ...
- JavaScript基础系列之四 面向对象编程
JavaScript基础系列之四 面向对象编程 面向对象编程 JavaScript的所有数据都可以看成对象,那是不是我们已经在使用面向对象编程了呢? 当然不是.如果我们只使用Number.Array. ...
- 最通俗易懂的JavaScript进阶教程
前言:"当你停止尝试时,就是失败的时候."你好!我是梦阳辰.快和我一起继续学习起来吧! 如果你对JavaScript了解甚少,也许下面一篇文章对你有用! 最通俗易懂的JavaScr ...
- javascript 学习笔记之面向对象编程(二):继承多态
~~接上篇~~上一篇实现了类的实现以及类成员变量和方法的定义,下面我们来了解下面向对象中两个最重要的特性:继承和多态. 继承 js中同样可以实现类的继承这一面向对象特性,继承父类中的所有成员(变量和属 ...
最新文章
- 美多商城之验证码(短信验证码1)
- WINCE下如何设置/删除/查询这些环境变量
- linux网卡热,linux网卡
- 单调不减序列查询第一个大于等于_[力扣84,85] 单调栈
- http1.0和http1.1和http2.0有什么区别
- 浏览器显示无法解析服务器的DNS地址,使用搜狗浏览器时突然弹出无法解析服务器的DNS地址该如何处理...
- 学会用taro封装一个组件
- dsp实现快速傅里叶的C语言程序,DSP-快速傅立叶变换(FFT)算法实验
- 在线生成艺术字_生成艺术:如何修改绘画
- 任正非:不要过度消费客户及民众对我们的同情与支持
- Django 第十课 1.【ORM模型】
- ios 镜像_2020年微软MSDN原版系统镜像下载 包含Windows10/7/8/8.1/XP系统
- 策略模式详解(用java语言实现策略模式)
- PYS60版短信搜索程序
- python操作jira修改status及写入comment
- 字节大幅裁员朋友圈刷爆网络:想给测试人提个醒...
- vue获取微信登陆权限_vue 微信授权登录解决方案
- 分享谷歌浏览器历史版本下载地址和谷歌浏览器驱动历史版本下载地址
- 计算机视觉编程 第六章 图像聚类
- 使用设计模式出任CEO迎娶白富美(7)–建造者模式解决工艺复杂问题