打败恶魔第一步,我们先要了解什么是 面向对象编程?
(1)什么是对象?
(2)什么是面向对象?
什么是对象?
对象是无序键值对的集合,其属性可以包含基本值、对象或者函数。
简单来说,我们知道数组是有序键值对的集合,那对象是无序键值对这个好理解吧。然后属性里面包含基本值(指的就是
简单的数据类型
),对象(对象里面也可以嵌套对象呀
),函数(其实就是方法
)
每个对象都是基于一个引用类型创建的,这些类型可以使系统内置的原生类型,也可以是开发人员自定义的类型。
系统内置的原生类型
开发人员自定义的类型(就是我们自己new出来的一个构造函数)
什么是面向对象?
面向对象编程-Object Oriented Programming 简称OOP
,是一种编程的开发思想
在面向对象的程序开发思想当中,每个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。因此,面向对象编程具有灵活
、代码可复用
、高度模块化
等特点,容易维护和开发
,比起由一系列函数或指令组成的传统式的过程式编程(procedural programming),更适合多人合作的大型项目。
上面一串文字让人看了很头痛,别慌,我们只要大概知道面向对象编程的好处有哪些,下面我们一起来对比下过程式编程和面向对象编程,可能会更好地理解这个概念。
面向对象与面向过程:
-
面向过程就是 亲力亲为,事无巨细,当然面向过程也是一种编程思想(但是偏向于
员工
的角度)
它的关注点在于解决问题的一个过程
(我要先干嘛,然后干嘛,再干嘛) -
面向对象就是找一个对象,让她去做这件事情(
老板
的角度)
它的关注点在找到解决问题的对象上面 -
面向对象并不是面向过程的替代,而是
面向过程的一个封装
面向对象编程的三大特征
封装性
:用对象封装,封装的更彻底继承性
:子承父业- 多态性:这个JS不支持(因为JS是一门弱类型的语言)
打败恶魔第二步 了解创建对象的方式有哪些?
创建对象的方式
1.new一个对象
缺陷:比较麻烦,每次添加属性都需要使用点语法
var obj = new Object()
obj.name = 'xh'
obj.age = 13
obj.sayHello = function () {
console.log('sayHello');
}
console.log(obj);
2.对象字面量{}
缺陷:每次 只能创建一个对象,不能批量地创建
var obj = {
name: 'xh',
age: 12,
sayHi: function () {
console.log("sayHi");
}
}
console.log(obj);
3.工厂函数
function creatObj(name, age) {
var obj = {
name: name,
age: age,
sayHi: function () {
console.log('i am 帅哥');
}
}
// 这里千万要注意return 出去,工厂函数才可以批量调用
return obj
}
// 这里需要找个变量进行接收,因为对象是函数里面return出来的
var lw = creatObj('lw', 37)
var xh = creatObj('xh', 15)
console.log(lw, xh);
缺陷:创建出来的对象具体类型无法识别(就只能知道是一个对象而已)
4.自定义构造函数
特征:首字母大写(规范)、构造函数需要配合new一起使用
function Person(name, age) {
// 构造函数中的this指向了新创建出来的对象
// this.xxx=yyy; 的形式来给新创建的对象添加属性和方法
this.name = name;
this.age = age
}
//new出来的对象 赋值给p1的是一个内存地址
var p1 = new Person('xh', 25)
console.log(p1);
这里面的new
做了四件事情:
- 创建了一个新的对象
- 把构造函数里面的this指向了新对象
- 执行构造函数里面的代码
- 把创建的新对象给返回出去
那这里面的构造函数
做了什么事情?
-回答:存储了代码,给this(新构造的对象)添加了属性和方法
自定义构造函数
可以解决工厂函数创建对象造成的对象无法识别对象类型的问题
这些专业的术语你要知道
实例对象
:构造函数创建出来的对象,实例对象可以有多个
实例化
:创建实例对象的过程
成员
:指的是对象的属性和方法
那自定义构造函数就没有自己的缺陷了吗?
回答:是有的,请看以下代码:
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHello = function () {
console.log("Hello");
}
}
var p1 = new Person('xh', 25)
var p2 = new Person('xm', 11)
console.log(p1 == p2); /*false*/
console.log(p1.sayHello == p2.sayHello); /*false*/
很奇怪的是p1和p2的sayHello 方法明明是同一个,为什么对比出来的结果却是false呢?
其实就是内存地址的问题
他们进行对比,对比的都是内存地址,但是他们的内存地址都是不一样的
上述的图片说明了,假如我创建了一千个自定义构建函数,那我是不是有一千个地址,但是我的方法的作用却是相同的,那么是不是存在着一个内存浪费的问题
。
那么我们要怎么去解决呢?
思路:让内存地址当中只有一份sayHello方法
下面只是一个过渡
的方法
// 将方法里面的函数移除外面来,让内存保证只有一个
var tools = {
fn1: function () {
console.log("Hellow");
}
}
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHello = tools.fn1;
}
var p1 = new Person('xh', 25)
var p2 = new Person('xm', 11)
console.log(p1.sayHello == p2.sayHello); /*true*/
但最好的解决方法是:通过原型来解决构造函数中的内存浪费问题
那么问题来了,原型是什么?
原型是什么?
打败恶魔第三步,了解原型是什么?
别急,我们慢慢来~我们先理一下
- 函数都有prototype属性,从侧面说函数也是一个对象
function Person() { }
console.dir(Person);
- 函数prototype的
属性值
是个对象,我们把这个对象称之为原型
(原型对象)
function Person() { }
console.log(Person.prototype);
原型对象的作用
通过构造函数构造出来的对象
可以直接访问构造函数的prototype属性上的任意成员
看以下的代码来理解
function Person() { }
// 给构造函数添加一个新的属性
Person.prototype.color = 'pink'
// 创建一个实例对象
var p1 = new Person()
// 实例对象可以直接访问构造函数的prototype属性上的任意成员
console.log(p1.color); /*pink*/
回到我们之前所提及的自定义构建函数造成的内存浪费问题,原型是怎么解决的呢?
function Person(name, age) {
this.name = name;
this.age = age;
}
//直接在原型上面添加方法
Person.prototype.sayHello = function () {
console.log("Hello");
}
var p1 = new Person('xh', 25)
var p2 = new Person('xm', 11)
p1.sayHello()
p2.sayHello()
//两个实例对象都可以调用到方法,而且内存地址也是一致的
console.log(p1.sayHello == p2.sayHello); /*true*/
图解构造函数、原型对象和实例对象之间的关系
__proto__属性
1.每个对象都有__proto__
属性
2.每个对象的__proto__
属性指向构造函数的prototype(原型对象)
function Person() {
}
var p1 = new Person()
console.log(p1.__proto__);
console.log(Person.prototype);
console.log(p1.__proto__ == Person.prototype); /*true*/
要想访问到原型对象,有两种的途径:
1.通过构造函数的prototype来访问
2.通过实例对象的__proto__来访问
__ proto__属性的注意点:
- 不是个标准的属性,存在兼容问题,IE678不识别该属性
- 注意最好别在线上代码中使用该属性
推荐做法
从构造函数的prototype访问原型对象,并且为它添加属性
constructor属性
- 原型对象上自带的constructor属性
- 原型对象的该属性值指向构造函数
function Person() {
}
var p1 = new Person()
console.log(Person.prototype.constructor);
接下来,我们要打终极恶魔了。
原型链究竟是什么呢?
原型链
先抛出一个问题
function Person() {
}
//实例对象p是怎么调用toString()方法的
var p1 = new Person()
p1.toString()
console.log(p1);
console.log(Person.prototype);
实例对象p是怎么调用toString()方法的?
明明 实例对象自己没有这个方法,原型对象也没有啊?
带着这个问题,我们一起来学习原型链。
原型链:任何对象都是有__proto__属性
,指向他的原型对象
,原型对象也是对象,那么原型对象也是有__proto__属性,指向的是原型对象的原型对象
,这样形成的一个链式结构叫做原型链
那么我们的首要任务就是 先找到原型对象的原型对象,那么要怎么找呢?
以我们刚才一直举的例子为例:
1.我们先把原型对象看成一个大类
2.那么 原型对象其实是可以根据__proto__来找到属于他自己的原型对象
(为了简单起见,这里把第一个原型对象,当做大头儿子,第二个原型对象的原型对象,当做小头爸爸
)
function Person() {
}
var p1 = new Person()
console.log(Person.prototype.__proto__);
但是log出来的是这样一个东西,我们看不懂
但我们可以看到log出来里面有个constructor
然后自然可以联想到
我们可以换一个角度去想,既然我们不知道小头爸爸(原型对象的原型对象)具体叫什么名字?
那我们可以想想,小头爸爸是不是也是一个原型对象,一个原型对象是不是有constructor属性?指向的是??
就是 Object对象这个构造函数
,那么我们是不是就可以知道小头爸爸的名字了?Object.prototype
然后顺着刚才的思路,我们继续再往上面去找,那小头爸爸有没有自己的爸爸呢?我们再通过__proto__来试试,会发现最后的结果是:
画个图来一起理解下:
(上图忘记写原型链的顶端了 那就是null)
说了那么多,其实实例对象p的原型链长的是咋样的呢?
p => Person.prototype => Object.prototype=>null
那回到我们刚才的问题
我们从刚才的原型链可知,其实是p从Object.prototype中拿到的属性toString()
另外补充一句,属性的查找原则:就是往上找
ok,我们的原型链还没完呢?
大家有没有感到好奇,其实函数也是一个对象啊?那函数的原型链是怎样的呢?
打败恶魔第四步,了解函数的原型链
我们离完整的原型链越来越近啦~
函数的定义类型有哪些?
函数的三种定义类型
// 1.函数声明
// 为什么fn()放在前面可以执行,因为函数声明会有预解析
fn()
function fn() {
console.log('fn')
}
// 2.函数表达式
// 为什么 fn2()放前面不执行,也是因为预解析,预解析只会提升变量,而不会提升赋值
fn2() /*执行 会报错 需要把fn2()放到后面去*/
var fn2 = function () {
console.log("fn2");
}
// 3.函数也是对象,对象是被new出来的
var fn3 = new Function('n1', "n2", 'bodyFn')
// 参数有若干个
// 参数都是字符串类型
// 这里new出来的对象,前面的参数都是函数的形参
// 最后一个参数,是函数的身体,也就是这个函数的内容
//举个栗子:
var fn4 = new Function('a', 'b', 'alert(a + b)')
// 这个fn4其实里面是什么
// var fn4 = function (a, b) {
// alert(a + b)
// }
fn4(1, 2)
我们可以知道,最后一种是很不日常的,我们很少去用,但是对于我们今天的原型链的思考却大有用处。
函数也是一个对象
我们先一起来绘制下关于函数的原型链
假如接着最上面的
function Person(){}
//var Person=new Function()
这个就是上面那个函数的底层
那么: 因为 Person是构造函数Function new出来的,所以Person是实例对象,而构造函数又可以通过.prototype访问实例对象,所以Function.prototype是Person实例对象的原型对象,而实例对象又可以通过__proto__访问原型对象。
所有的函数都是Function的实例
我们再通过代码,log一下,看下他们的表现形式
Function.prototype
在js中原型对象当中唯一类型为函数的(但是函数也是对象呀,所以不冲突)
可以看到Function.prototype.proto log 出来的对象的constructor是 f Object,所以可以知道我们Function.prototype他的原型对象 也是Object.prototype
所以我们又可以完善一下我们的绘图
但是以上的原型链都是不完整的,
接下来,我们一步一步地将他完善
完整的原型链
共有五部曲:
1.把函数当成函数看,具体可以当做构造函数来看
构造函数:Person
原型对象:Person.prototype
实例对象:p
function Person() {
}
var p = new Person()
绘制图如下
2.把函数当对象看,具体是看成实例对象
构造函数:Function
原型对象:Function.prototype
实例对象:Person
var Person=new Function()
3.把对象当函数看,具体当做一个构造函数
构造函数:Object
原型对象:Object.prototype
实例对象:obj
var obj = new Object()
4.把对象当成一个实例对象
因为所有函数都是Function的实例对象(因为底层都是Function new出来的)so:-
构造函数:Function
原型对象:Function.prototype
实例对象:Object
var Object=new Function()
5.把函数当成是一个实例对象(最难理解)
因为函数也是一个对象(然后底层是把它设计成也是个实例对象)
构造函数:Function
原型对象:Function.prototype
实例对象:Function
最完整的图
完整原型链小结:
- 所有对象的原型链上面都有Object.prototype
- 所有函数的原型链上面都有Function.prototype
- 所有的对象都有__proto__属性 、所有的函数都有prototype属性,又因为函数也是对象,所以:
函数既有prototype属性,也有__proto__属性
原型链测试题
console.log(Object.__proto__ === Function.prototype);
console.log(Function.prototype === Object.prototype);
console.log(Object.prototype.__proto__ === Object.prototype);
console.log(Object.__proto__.__proto__ === Object.prototype);
answer: