面向对象
1. JavaScript 面向对象的介绍
- 、什么是对象
Everything is object (万物皆对象)
对象到底是什么,我们可以从两个层次来理解。
(1) 对象是单个事物的抽象。
一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个与远程服务器的连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。
(2) 对象是一个容器,封装了属性(property)和方法(method)。
属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal对象,使用“属性”记录具体是那一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。
在实际开发中,对象是一个抽象的概念,可以将其简单理解为:数据集或功能集。
ECMAScript-262 把对象定义为:无序属性的集合,其属性可以包含基本值、对象或者函数。严格来讲,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。
提示:每个对象都是基于一个引用类型创建的,这些类型可以是系统内置的原生类型,也可以是开发人员自定义的类型。
- 、什么是面向对象
面向对象不是新的东西,它只是过程式代码的一种高度封装,目的在于提高代码的开发效率和可维护性。
面向对象编程 —— Object Oriented Programming,简称 OOP ,是一种编程开发思想。它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。
- 、面向对象与面向过程
面向过程就是亲力亲为,事无巨细,面面俱到,步步紧跟,有条不紊
面向对象就是找一个对象,指挥得结果
面向对象将执行者转变成指挥者
面向对象不是面向过程的替代,而是面向过程的封装
- 、面向对象的特征
封装性
继承性
多态性
2.回顾创建对象的四种方法
回顾创建对象的四种方式
// 字面量 | ||
var button = { | ||
wdith: 20, | ||
height: 20, | ||
color: "red", | ||
click: function () { | ||
console.log("点击") | ||
}, | ||
enter: function () { | ||
console.log("鼠标进入") | ||
} | ||
} | ||
console.log(button); | ||
button.click() | ||
button.enter() | ||
// new Object()创建 | ||
var button = new Object(); | ||
button.wdith = 20; | ||
button.height = 20; | ||
button.color = "red"; | ||
button.click = function () { | ||
console.log("点击") | ||
}; | ||
button.enter = function () { | ||
console.log("鼠标进入") | ||
} | ||
console.log(button); | ||
button.click() | ||
button.enter() 工厂函数 | ||
function cButton(wdith, height, color) { | ||
var button = {} | ||
button.wdith = wdith; | ||
button.height = height; | ||
button.color = color; | ||
button.click = function () { | ||
console.log("点击") | ||
}; | ||
button.enter = function () { | ||
console.log("鼠标进入") | ||
} | ||
return button; | ||
} | ||
var bu = cButton("20", "20", "red") | ||
console.log(bu); | ||
bu.click(); | ||
bu.enter(); | ||
</script> | ||
<script> | ||
// 自定义构造函数 | ||
function button (wdith,height, color){ | ||
this.wdith = wdith; | ||
this.height = height; | ||
this. color = color; | ||
this.click = function () { | ||
console.log("点击") | ||
} | ||
this.enter = function(){ | ||
console .log("鼠标进入") | ||
} | ||
} | ||
var but = new button ("20","20","red") | ||
console.log(but) | ||
but.click(); | ||
but.enter(); |
3.构造函数和实例对象的关系
(1)、解析构造函数代码的执行
创建一个实例对象,必须使用 new 操作符。以这种方式调用构造函数会经历以下 4 个步骤:
1、创建一个新对象
2、将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)
3、执行构造函数中的代码
4、返回新对象
(2)、constructor属性
对象的 constructor 属性最初是用来标识对象类型的
可以通过实例的 constructor 属性判断实例和构造函数之间的关系
构造函数实例化对象的constructor属性指向的是构造函数本身
要检测对象的类型,使用 constructor操作符更可靠一些,返回true为对象
例如:obj.constructor == Object
(3)、instanceof关键字
如果要检测对象的类型,还是使用 instanceof 操作符更可靠一些,返回true为对象
obj instanceof Object
总结:
构造函数是根据具体的事物抽象出来的抽象模板
实例对象是根据抽象的构造函数模板得到的具体实例对象
每一个实例对象都具有一个 constructor 属性,指向创建该实例的构造函数
4.构造函数的问题
(1)、内存浪费问题
使用构造函数带来的最大的好处就是创建对象更方便了,但是其本身也存在一个浪费内存的问题
对于每一个实例对象,如果我们在一个实例对象的内部创建一个属性,值为函数。假如创建两个对象,属性名也许一致,看似都是一模一样的内容,但是其实每一次生成一个实例,都会多占用一些内存,如果实例对象很多,会造成极大的内存浪费。
利用把属性值定义在外面方法,我们可以解决数据共享的问题,但是如果有多个需要共享的函数的话就会造成全局命名空间冲突的问题。
你肯定想到了可以把多个函数放到一个对象中用来避免全局命名空间冲突的问题:
至此,我们利用自己的方式基本上解决了构造函数的内存浪费问题。但是代码看起来还是那么的格格不入,那有没有更好的方式呢?
练习:1.先创建一个构造函数,比如水果类,然后依次创建苹果,香蕉,橘子实例对象,其中要求他们又都能创造新果实作为一个方法,要求所有的实例对象的这个方法是同一个,并且在外面再取一个相同名字的方法与实例对象的方法互不影响。
5.原型
(1)、原型
更好的解决方案: prototype
Javascript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。
这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype 对象上。
function Person (name, age) {
this.name = name
this.age = age
}
console.log(Person.prototype)
Person.prototype.type = 'human'
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__ 是非标准属性。
实例对象可以直接访问原型对象成员。
总结:
1、任何函数都具有一个 prototype 属性,该属性是一个对象
2、构造函数有一个protoType属性,它本身是一个对象,我们称之为原型
3、构造函数的protoType原型对象的属性和方法,都可以被构造函数实例化的对象所继承
4、构造函数的protoType原型对象有个constructor属性,指向的是当前原型对象所在的构造函数
5、实例对象有__proto__属性,它是一个指针,指向的是构造函数的的原型prototype
6、实例对象都具有一个 constructor 属性,指向创建该实例的构造函数
(2)、实例化对象的查找规则
了解了 构造函数-实例-原型对象 三者之间的关系后,接下来我们来解释一下为什么实例对象可以访问原型对象中的成员。每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。
搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值。如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。
如果在原型对象中找到了这个属性,则返回该属性的值也就是说,在我们调用对象.属性的时候,会先后执行两次搜索:
首先,解析器会问:“实例 person1 有 sayName 属性吗?”答:“没有。
”然后,它继续搜索,再问:“ person1 的原型有 sayName 属性吗?”答:“有。”于是,它就读取那个保存在原型对象中的函数。当我们调用 person2.sayName() 时,将会重现相同的搜索过程,得到相同的结果。
而这正是多个对象实例共享原型所保存的属性和方法的基本原理。
总结
先在自己身上找,找到即返回,自己身上找不到,则沿着原型链向上查找,找到即返回,如果一直到原型链的末端还没有找到,则返回 undefined
实例对象读写原型对象成员
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype = {
type: 'human',
sayHello: function () {
console.log('我叫' + this.name + ',我今年' + this.age + '岁了')
}
}
在该示例中,我们将 Person.prototype 重置到了一个新的对象。这样做的好处就是为 Person.prototype 添加成员简单了,但是也会带来一个问题,那就是原型对象丢失了 constructor 成员。
所以,我们为了保持 constructor 的指向正确,建议的写法是:
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype = {
constructor: Person, // => 手动将 constructor 指向正确的构造函数
type: 'human',
sayHello: function () {
console.log('我叫' + this.name + ',我今年' + this.age + '岁了')
}
}
6.额外的一些技巧
利用自调用函数把局部变量变为全局变量。
在之前我们就学习了js的自调用函数,那么自调用函数的作用主要是什么呢?避免变量污染,有哪些应用场景呢?比如封装插件。
那如果此时我们想要把局部变量变为全局变量呢?
函数是有参数的,我们可以通过把window对象传入而解决。
7.改变this指向的方法
(1)、call方法
1、call()方法可以进行普通函数的调用
2、call()方法可以改变this的指向,如果没有参数,this指向window
3、call()方法可以改变this的指向,如果有一个参数,this指向该参数
4、call()方法可以改变this的指向,如果有多个参数,this指向第一个参数,剩下的是个参数列表(构造函数继承的案例)
(2)、apply方法
1、 apply()方法可以进行普通函数的调用
2、apply()方法可以改变this的指向,如果没有参数,this指向window
3、apply()方法可以改变this的指向,如果有一个参数,this指向该参数
4、apply()方法可以改变this的指向,如果有多个参数,第一个参数是null或者window,第二个参数是数组
var arr = [11, 55, 33, 66, 88];
console.log(Math.max(11, 22, 33));//33
console.log(Math.max(arr));//NaN
console.log(Math.max.apply(window, arr));//88
console.log(Math.max.apply(null, [11, 55, 33, 66, 88]));//88
(3)、bind方法
推荐使用第二种形式,第三种用的相对较少,但也是必须掌握的内容。
bind() 函数会创建一个新函数(称为绑定函数),新函数与被调函数(绑定函数的目标函数)具有相同的函数体(在 ECMAScript 5 规范中内置的call属性)。当目标函数被调用时 this 值绑定到 bind() 的第一个参数,该参数不能被重写。绑定函数被调用时,bind() 也接受预设的参数提供给原函数。一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
- bind()不能进行函数的调用
- 可以改变this指向
var name = "小明";
var obj3 = {
name: "小刚",
getName: function () {
var name = "小熊";
console.log(this.name);//小刚
console.log(name);//
that = this;
/* that = this
var fn2 = function () {
console.log(this.name);//小明 //小刚
}.bind(that); */
var fn2 = function () {
var name = "小李";
console.log(this.name);//小明
console.log(name);//小李
}.bind(that);
fn2();
}
}
obj3.getName();