封装和继承
面向对象的核心是什么?
面向对象的核心是『类』,但是JS中没有类,只有『构造函数』
JS中构造函数和类是同一个东西,『类』和『构造函数』都是返回对象的函数
面向对象要解决的问题是什么?
表面上看是为了实现封装、继承和多态(并非只有面向对象可以,函数式也有)
但是终极目的是为了实现写代码的时候不用再思考如何写代码
面向对象解决的是写代码的套路问题(定势思维)
遇到任何需求都用一套方法去解决,不用去思考,照着套路去做就可以了
打个比方:如何成为富人?
穷人不用去思考如何成为富人,只要模仿让有钱人变有钱人的行为,照着去做就行了,减少大脑的思考
封装
封装就是『隐藏细节』
- A对A
- A对B
A对B(自己对别人或者说别人对自己)
作用:提供优雅的API,有助于更多的人在一起合作
如:jQuery的作者提供API:$.get(url).then(success)
A对A(自己对自己)
作用:解决思维负担(因为通过函数名就知道大概要实现的功能)
如:写了一百行代码,内容是『如何同女孩搭讪?』,具体的流程和细节不想看,就把一百行代码放到一个函数里,传一个参数
function 跟女孩搭讪(女孩的名字){
// 此处省略100行代码
}`
继承
继承是为了『复用』代码
为什么要有继承
举例子: 弹窗有很多种,如:基本窗口、对话框窗口、提示窗口等
但是每种窗口可能会有很多相同的地方
比如每种窗口都有如下的『属性』:是否有关闭按钮、是否有标题、宽高的不同、是否有确认取消按钮等
每种不同的窗口只是这些『不同属性』的任意组合
如果没有继承,那么每种窗口都各自实现各自的,有些彼此都有的属性,就要重复实现一遍,效率很低
但是有了继承后,我们可以提取所有窗口『相同的属性』,比如:大小都一样、都有关闭按钮(继承的意思就是"同上")
落实到具体的窗口可以继承上面『相同的属性』,彼此实现自己『独有的属性』
如确定取消按钮的文字可能不一样,窗口的颜色可能不一样等
这样只要实现彼此间『不同属性』的代码就可以了,提升了效率
如何实现继承?
1.面向对象中通过关键字『extend』
用上面不同的弹窗举例:
// 对话框继承基础窗口
class DialogWindow extend Window
// 提示窗继承基础窗口
class PopWindow extend Window
2. JS中通过原型实现
多态
一种东西拥有两种甚至多种东西的属性
多态是为了让一个东西『更加的灵活』
为什么要有多态?
举例子:div可以同时是节点和元素
1. 把div当做节点用,获取div作为节点子代
div.childNodes
2. 把div当做元素用,获取div作为元素后代
div.children
原型链(对象和对象)
JS它有什么(去实现面向对象)?
七种数据类型
- number
- string
- bool
- undefined
- null
- symbol
- object
前6种是基本类型,在内存中存的是值。object叫复杂类型,也叫引用类型,在内存中存的是内存地址,我们需要关注的是object
object的分类
- 普通对象,如
{name:''lee}(下标是随意的) - 数组(下标是特定的,0、1、2、... length)
- 函数
为什么要有原型链?
结论: JS中使用原型链将对象串连起来
0.对象在内存中是怎样的?
当我们写了下面的代码
var obj = {name: 'lee', age: 18}
内存中是这样:
备注: Stack是栈内存
1.有name和age属性可以理解,为什么obj有toString属性? toString哪里来的?
2.声明对象的时候"偷偷的"加toString
如果想让obj有toString属性,最直接的方法就是把toString属性加到内存中
当声明一个对象的时候,就往内存中偷偷的加一个toString属性
3. 如果声明了另一个对象obj2呢?
4. 发现存在的问题
-
内存中重复的存了相同的key( name、age)
-
地址为206的内存和地址为133的内存中内容是完全一样的,为什么要重复存呢?
5.那就让相同的属性共用同一块内存吧
6. 如果除了共有toString()还共有valueOf()呢?
7. 如果obj和obj2有10000个相共有的属性呢?
那么存在的问题是:obj和obj2就重复的写了10000次相同的属性,造成了很多重复的key
8. 来统一key吧!把共有的属性放到一个对象里
内存声明一个对象(地址188),专门存放obj和obj2『共有的属性』
当我们写了obj.toString()的时候,JS引擎就先到obj内存找有没有toString属性(89号内存)
如果没找到就到『共有属性』中找,发现共有属性中有toString,于是obj就有toString方法了(188号内存)
JS就通过这种小技巧:
在声明的对象时添加一个『共有属性』
如果想要共享一个属性
就将所有要共享属性单独放的做成一个对象,放到一块内存中(188号内存)
让『共有属性』去使用它
这就是原型链最朴素的想法:『省内存』
JS中如何实现原型链?
所谓的原型就是『共有属性』
从上图可以看出obj和obj2的原型[Prototype]也就是对象(也就是『共有属性』)
实际上就是对应的是同一个内存地址
就好比是上面例子中的188号内存
这块内存中就放了所有对象共有的属性
如果往原型上添加属性,内存图中发生了什么?
通过对象的__proto__来访问原型
当我们写obj.__proto__.gender='男'的时候
实际上就是往内存地址为 Addr 188 的内存中,往里面写一个属性叫gender
当JS引擎去obj2中找gender发现obj2中没有gender(34号内存),于是就去它的『共有属性』中找,发现里面有一个属性gender
备注:
1.内存是程序员首要理解的东西,写的每一行代码都要知道在内存中发生了什么
2.理解原型的时候可以使用
obj.__proto__这样访问,但是生产环境下千万不能这样访问一个对象的原型,否则会造成性能上的降低
如何正确访问一个对象的原型
JS标准规定,用来放所有对象共有属性的地方(图中的188号内存)叫『Object』
『共有属性』是天生就有的
就算没有声明obj和obj2,用来放所有对象共有属性的对象(上图中的188号内存)就已经在内存中了,等着我们用『共有属性』去"共"它
在内存中的模样:
如何实现数组中有push()方法而对象中没有push()方法?
按照目前的结论共有属性中只有toString()方法和valueOf()等方法,如何才能实现数组中有push()方法而对象中没有push()方法?
解决办法:再到内存中声明一个新的对象(内存地址为11),这个对象专门存放数组的『共有属性』
如何实现arr.valueOf()?
解决办法: 让存放数组共有属性的对象(11号内存),再接着去『共』之前对象的『共有属性』(188号内存),这样数组中也有了valueOf()方法
看下实现arr.valueOf()过程:
- 去203地址找发现没有valueOf()
- 于是就去它的『共』去找,『共』中存的对象的地址是11号内存
- 于是就去11号内存找,发现11号内存中也没有valueOf()
- 于是就去11号内存的『共』去找,『共』中存的对象的地址是188号内存
- 最终在188号内存中找到了valueOf()
备注
1.JS中没有类,全是对象,对象和对象之间通过地址相互引用
2.图中的内存地址只是随机举例子,并非实际真实的内存地址
3.JS中这样的设计很巧妙,每一个对象默认都有一个叫『共』的属性,『共』对应的内存中存放的就是它和其他对象共用的属性
4.『共』的真正名字是
__proto__
原型链
- 每一个对象都有一个『共』的属性,这个『共』存的是另一个对象的地址 188 (
window.Object.Prototype) - 每一个数组也有一个『共』的属性,这个『共』存的是另一个对象的地址11它是新的对象,这个对象中存的是数组共有的属性、函数(
window.Array.Prototype) - 这个新的对象(地址为11)(
window.Array.Prototype)中也有一个『共』的属性可以『共』188 - JS通过这样对象和对象之间相互引用,再人为的加上线(实际中并没有),就形成了原型链
原型链在JS内存中是一个树形结构
小写的object和大写的Object的区别?
从上图可以看出,大写的Object是天生就有的、不写任何代码就有的对象(大写的Array也是一样),都是JS为我们准备好了的最原始对象
把函数的原型也加进来
函数就是一个包含了代码的对象,这个代码就是字符串,它也有一个『共』的属性
为什么说JS中的Object是一切对象根源呢?
=> 因为它是树形结构中最顶端的东西,所有的对象都『共』用了它
总结
JS中没有类,JS只需要给每一个对象加一个__proto__(也就是『共』)属性就可以实现对象之间的相互引用,也就是说通过原型链就可以实现继承
如果不理解原型,就想一下『共』
以上就是JS中的对象是如何串连起来的 --- 原型链
this(对象和函数)
什么是函数?
函数是一种 可执行代码 组成的 对象
为什么函数是对象?
使用对象模拟函数
var obj = {
name: 'f',
length: 2,
// 函数的参数
params:['x','y'],
functionBody: 'console.log("Hello World")'
}
var objGong = { call: function(x){
// eval:执行字符串
eval(x.functionBody)
}
}
obj.__proto__ = objGong
obj.call(obj)
从上图可以看出函数就是这样的对象: 它拥有name、length、params、functionBody以及call方法,当调用这个函数的时候实际上就是eval字符串'console.log("Hello World")'
当我们写了下面的代码
function f(){
console.log(1)
}
f.call()
// 1
当我们写了上面的代码后,实际上就是在内存中声明了一个对象,它的name是f、functionBody是字符串 'console.log("Hello World")'
当我们调用这个对象上的call方法后,实际上就是eval()一下这个字符串
函数的本质是字符串加上其他东西组成的对象
如何将函数和对象联系起来?
1.当前函数和对象没有任何关联
function sayName(name){ console.log('I am'+ name) return true }
var obj = {name:'Lee'}
sayName(obj.name)
// 结果: I am Lee
2.将函数sayName放到obj里面呢?
var obj = {
name:'Lee',
sayName:function(name){
console.log('I am'+ name)
return true
}
}
obj.sayName(obj.name)
obj.sayName和函数没有任何关系,只不过obj.sayName恰好存了函数的地址
obj.sayName不是这个函数的本身,函数只是那一块内存
这样写没有任何好处啊
因为函数是函数,对象是对象。函数sayName除了接受一个参数,return一直值之外同对象毫无关系啊
为什么不这样写?
function sayName(name){ console.log('I am'+ name) return true }
var name = 'Lee'
sayName(name)
如果对象想要使用这个函数,那么可以把整个对象传给函数
3. 进行改进
var obj = {
name:'Lee',
sayName:function(x){
console.log('I am'+ x.name)
return true
}
}
obj.sayName(obj)
4. 为什么obj.sayName()不能获取到点前面的东西呢?
为什么obj.sayName()不能直接获取到obj.name作为参数调用呢? 不能,JS中只能接受"()"里面的东西作为参数,并return返回一个值(默认return undefined),就是这么的纯粹,并不能获取到点前面的东西
5. 能不能从浏览器的角度解决这个问题
如果用户没有传参数,浏览器能不能帮我们去传呢?
obj.sayName()当我们写这句话的时候难道不就是想操作obj这个对象吗?那不就是想把这个对象当做参数参数传进来?
因此浏览器就可以做如下的预测:如果一个函数是通过obj.fn(),那么就表示其实你是想操作点前面的对象,那么浏览器就会把点前面的obj作为参数传进去
也就是说obj.sayName = obj.sayName(obj)
但是问题是声明函数的时候参数怎么写?
你不传参数,但是声明函数的时候又使用参数x去接收?
var obj = {
name:'Lee',
sayName:function(x){
console.log('I am'+ x.name)
return true
}
}
obj.sayName()
JS规定如果调用函数的时候没有传参数,那么声明函数的时候就不能用参数x接收,那我现在怎么获取到浏览器帮我们传的参数obj呢?
===> 使用this
6.使用this改进
用this获取到sayName点前面的东西
var obj = {
name:'Lee',
sayName:function(){
console.log('I am'+ this.name)
return true
}
}
obj.sayName()
// I am lee
这就是this的第一个最用: 连接对象和函数
让浏览器帮我们『隐式的传参数』是『有魔法』的调用函数的方式
如果我们不想要浏览器『隐式的传参数』或者说隐式的指定this,我们就使用call的方法来调用函数
var obj = {
name:'Lee',
sayName:function(){
console.log('I am'+ this.name)
return true
}
}
obj.sayName.call()
// I am
由于this就是call的第一个参数,如果不传就是undefined
var obj = {
name:'Lee',
sayName:function(){
console.log('I am'+ this.name)
return true
}
}
obj.sayName.call(obj)
// I am lee
上面这样就是显示指定this(建议使用这种方法调用函数,这样能掌控this),在这里this仅仅只是一个参数
记住一个结论:
当写代码
obj.sayName()请在大脑中做这样的转换obj.sayName.call(obj)
看一个例子
var baba = {
name: 'WangJianLin',
child:{
name:'SiCong',
sayName: function(){
console.log(this.name)
}
}
}
1. baba.child.sayName() // SiCong
2. baba.child.sayName.call() // undefined,浏览器环境下会变成window
baba.child.sayName.call() === baba.child.sayName.call(undefined)
如果不想要浏览器把undefined改成window,可以使用严格模式(严格模式的意思就是"不要给我乱改")
注意:
baba.child.sayName.call()中的对象baba.child.sayName同函数sayName没有任何关系!!!
比如:
var baba = {
name: 'WangJianLin',
child:{
name:'SiCong',
sayName: function(){
console.log(this.name)
}
}
}
baba.child.sayName.call({name:'MaYun'}) // MaYun
baba.child.sayName 只不过存的地址
就是函数的地址
function(){
console.log(this.name)
}
所以这个函数始终是独立于这个对象存在的,函数不是对象的『附属品』
函数是『一等公民』
用内存图理解选函数和对象的关系
图中的sayName 和 101 中的函数有关系吗?
没有任何关系,101对象可以给任何人存
JS中函数永远是不跟对象"共舞"的,函数就是函数,它就是『独立的对象』(只接受一个输入,返回一个输出,是一个很纯粹的对象)
不像JAVA中函数是对象的『附属品』,因此JS中函数是『一等公民』
JS中函数的奇葩之处
使用call的方式调用函数,那么函数默认接受『2个参数』
function f(){
this // 第一个参数
arguments // 其他参数
}
this原本不属于JS语言,单纯只是为了长得像JAVA
原本使用arguments表示所有参数,改为了this是第一个参数,arguments为其他参数
this有2种形式:
- 显示的指定this(使用f.call())
- 隐式的指定this(直接f(),让浏览器去猜this是什么)
一个推论
1. 函数参数的值只有在传参的时候确定
由于this是一个参数,那么参数的值是如何确定的?
例如:函数f的参数a的值是什么时候确定的?
function f(a){
}
=> 参数a的值是函数f在被调用时确定的,传的什么值,a就是什么值
2. this是第一个参数(使用call的方式调用函数)
3. 推论: this的值只有在传参的时候确定
this的值是一个参数,我们怎么能提前知道它传的是什么参数呢,this是一个不确定的东西
this面试题
1. 函数未调用时的this
function a(){
console.log(this)
}
=> this的值不确定,因为this的值是在传参的时候确定
2. 函数调用时的this
function a(){
console.log(this)
}
a() // window (在浏览器中执行)
3. 严格模式下的this
function a(){
'use strict'
console.log(this)
}
a() // undefined
=> use strict的意思就是"别帮我乱改"
4. 对象中调用函数时的this
function a(){
console.log(this)
}
var obj = {
sayThis:a
}
obj.sayThis() // obj
5. 使用call的方式调用函数时的this
function a(){
console.log(this)
}
var obj = {
sayThis:a
}
obj.sayThis.call() // undefined
6. 绑定事件时的this
button.onclick = function(){
console.log(this) // button
}
按钮被点击时调用函数时call传的第一个参数是什么?
是this,这点浏览器的文档可以保证,看下面查mdn文档的结果
7. jQuery中事件的this
$('#button1').on('click',function(){
console.log(this) // $('#button1')对应的元素
})
查下jquery的文档才能确定this
发现一个问题:说了半天的this,但是我们依然确定不了this,为什么呢?
因为this的值只有在传参的时候确定
题6、题7中的函数都不是我们去调用的,而是浏览器帮我们调用的,浏览器调用函数的时候传的什么参数,我们怎么知道,只有浏览器知道,因此要去查文档
当jQuery在调用处理程序时,
this关键字指向的是当前正在执行事件的元素。对于直接事件而言,this代表绑定事件的元素。对于代理事件而言,this则代表了与selector相匹配的元素。(注意,如果事件是从后代元素冒泡上来的话,那么this就有可能不等于event.target。)若要使用 jQuery 的相关方法,可以根据当前元素创建一个 jQuery 对象,即使用$(this)。
7. 事件代理中的this
$('ul').on('click','li',function(){
console.log(this) // li
})
8. vue中的this
new vue({
data: function(){
console.log(this) // this是new出来的对象(vue文档)
}
})
如果再严谨点其实上面的this有些还是不对的,上面的情况只是默认了不使用call()的方式调用函数来指定this
比如:使用call调用函数,使用call的第一个参数显示的指定this的值,那么想传什么,this就是什么
button.onclick = function(){
console.log(this)
}
button.onclick.call({name:'hi'})
结论
this的值不确定,因为它是一个参数,我们怎么能凭空知道参数a的值是什么?
"我们怎么知道参数a的值是什么?你传给我什么,就是什么"
因this闹出的荒谬
例子: 因为2处的this是window,但是我想让3处的this是button
button.onclick = function(){
// 1处的this
this.disabled = false
$.get('/xxx.json',function(){
// 2处的this
console.log(this) // window(浏览器调用函数的时候什么参数都不传)
// 3处的this
this.disabled = false
})
}
于是就有人想出了个"好办法" 在1处用变量that(或者_this)记住this
button.onclick = function(){
// 1处的this
this.disabled = false
var that = this
$.get('/xxx.json',function(){
// 2处的this
console.log(this) // window(浏览器调用函数的时候什么参数都不传)
// 3处的this
that.disabled = false
})
}
整理下这种想法的逻辑:
- "我想要this,但是发现this不是我想要的"
- "于是我用另一个变量伪装this"
- 那么到底是想用this还是不想用this?this已经是错的,还要为了用this而使用this
- 用了一个无意义的名字来表示一个不能使用的变量
既然用的已经不是this,为什么不起一个自己一眼就能看出来的变量?还要用_this来迷惑自己
button.onclick = function(){
// 1处的this
this.disabled = false
var btn = this
$.get('/xxx.json',function(){
// 2处的this
console.log(this) // window(浏览器调用函数的时候什么参数都不传)
// 3处的this
btn.disabled = false
})
}
JS痛改前非,禁掉了this ---- 箭头函数
箭头函数中的this非调用时才确定
例如: 下面的this的值并不是call的时候指定的而是在声明f之前this就已经是window了
箭头函数中是以它外面的this来作为里面的this
箭头函数中既没有this也没有arguments
箭头函数中的this不接受传call的第一个参来指定,就算传了也不起作用,也就是直接把call的第一个参数给禁掉了
function中的this是不确定的,箭头函数中的this是确定的
button.onclick = function(){
// 1处的this
this.disabled = false // this不确定,因为它在function中
var f = ()=>{
// 2处的this
this.disabled = false // 此处的this就是外面1处的this
}
$.get('/xxx.json',f)
}
建议:永远不要使用this
this在值总是一个不确定的存在
比如:只写一个this,那么它是什么?
- 在浏览器环境下,它是window
- 在Node环境下,它是global
- 在严格模式下,它是undefined
new 用法
为什么要有new
new的出现是基于一个需求:批量创建的对象
没有new之前如何批量创建的对象?
拿红警游戏中兵营制造士兵来举例子
制造一个士兵
var 士兵 = {
ID: 1, // 用于区分每个士兵
兵种:"美国大兵",
攻击力:5,
生命值:42,
行走:function(){ /*走俩步的代码*/},
奔跑:function(){ /*狂奔的代码*/ },
死亡:function(){ /*Go die*/ },
攻击:function(){ /*糊他熊脸*/ },
防御:function(){ /*护脸*/ }
}
兵营.制造(士兵)
制造100个士兵
var 士兵们 = []
var 士兵
for(var i=0; i<100; i++){
士兵 = {
ID: i, // ID 不能重复
兵种:"美国大兵",
攻击力:5,
生命值:42,
行走:function(){ /*走俩步的代码*/},
奔跑:function(){ /*狂奔的代码*/ },
死亡:function(){ /*Go die*/ },
攻击:function(){ /*糊他熊脸*/ },
防御:function(){ /*护脸*/ }
}
士兵们.push(士兵)
}
兵营.批量制造(士兵们)
存在的问题
函数是对象,对象在内存中存的是地址
每个士兵函数都是创建的新对象
因此它们的行走、奔跑、死亡等函数在内存中都是不同的
士兵除了ID和生命值不一样,其他的动作如:行走、奔跑、死亡等代码都是一样的
每个函数都被重复了100次
这样就造成了很多内存的浪费
根据前面说到的原型链的知识,我们可以把行走、奔跑、死亡等函数只写一份放到一个内存中
作为所有士兵的共有属性来使用
解决问题
JS中『共』叫__proto__
这样就节省了很多内存
使用原型来改进
我们创建一个『士兵原型』来作为所有士兵共有的属性(内存地址为A666的内存),让所有士兵的__proto__指向这个士兵原型(__proto__就是之前谈及的『共』)
除了函数,属性也可以放到士兵原型中("都是美国大兵,那么兵种和攻击力一般是一样的")
var 士兵原型 = {
兵种:"美国大兵",
攻击力:5,
行走:function(){ /*走俩步的代码*/},
奔跑:function(){ /*狂奔的代码*/ },
死亡:function(){ /*Go die*/ },
攻击:function(){ /*糊他熊脸*/ },
防御:function(){ /*护脸*/ }
}
var 士兵们 = []
var 士兵
for(var i=0; i<100; i++){
士兵 = {
ID: i, // ID 不能重复
生命值:42
}
/*实际工作中不要这样写,因为 __proto__ 不是标准属性*/
士兵.__proto__ = 士兵原型
士兵们.push(士兵)
}
兵营.批量制造(士兵们)
看下运行结果
用函数进行封装
var 士兵原型 = {
兵种:"美国大兵",
攻击力:5,
行走:function(){ /*走俩步的代码*/},
奔跑:function(){ /*狂奔的代码*/ },
死亡:function(){ /*Go die*/ },
攻击:function(){ /*糊他熊脸*/ },
防御:function(){ /*护脸*/ }
}
function 士兵(){
var obj = {
ID: i,
生命值: 42
}
obj.__proto__ = 士兵原型
return obj
}
var 士兵们 = []
for(var i=0; i<100; i++){
士兵们.push(士兵(i))
}
兵营.批量制造(士兵们)
代码太松散了
创建一个士兵的代码分散在两个地方,一个是士兵原型,另外一个是士兵函数,他们两没有关联,我们得让看代码的人一眼就能看出来,这两个部分是有关联的(将士兵原型作为士兵的属性),即使是从名字上看这2部分是不能分开的
// 1. 构造函数
function 士兵(ID){
var obj = {
ID: i,
生命值: 42
}
obj.__proto__ = 士兵.原型
return obj
}
// 函数也是对象
士兵.原型 = {
兵种:"美国大兵",
攻击力:5,
行走:function(){ /*走俩步的代码*/},
奔跑:function(){ /*狂奔的代码*/ },
死亡:function(){ /*Go die*/ },
攻击:function(){ /*糊他熊脸*/ },
防御:function(){ /*护脸*/ }
}
// 2. 使用构造函数
var 士兵们 = []
for(var i=0; i<100; i++){
士兵们.push(士兵(i))
}
兵营.批量制造(士兵们)
改下变量名吧
『原型』只是英文『prototype』翻译过来的叫法
// 1. 构造函数
// soldier是构造函是因为它返回了新的对象
function soldier(ID){
var obj = {
ID: i,
生命值: 42
}
obj.__proto__ = soldier.prototype
return obj
}
// 函数也是对象
// 名字叫soldier.prototype的目的是为了让它看起来和士兵有关联
soldier.prototype = {
兵种:"美国大兵",
攻击力:5,
行走:function(){ /*走俩步的代码*/},
奔跑:function(){ /*狂奔的代码*/ },
死亡:function(){ /*Go die*/ },
攻击:function(){ /*糊他熊脸*/ },
防御:function(){ /*护脸*/ }
}
然后就可以引用「士兵」来创建士兵了:
// 2. 使用构造函数
var soldiers = []
for(var i=0; i<100; i++){
soldiers.push(soldier(i))
}
兵营.batchMake(soldiers)
问题:
为什么士兵函数里面要声明对象obj? 因为要返回一个对象,当然要声明一个对象
为什么把共有属性的名字叫“士兵.原型”? 因为为了让“士兵.原型”从名字上看起来和士兵有关联,让代码看起来不再松散
原型是什么意思? 原型是英文翻译过来的(prototype),中文叫共有属性,它只是一个名字而已,叫什么真的无所谓,只要你知道它的本质是什么就行,你乐意叫xxx也行
为什么
obj.__proto__ = soldier.prototype中左边prototype的有__而右边的没有 它们只是一个名字而已,在内存中obj.__proto__和soldier.prototype存的都是相同的内存地址(存的是同一个对象,对象在内存图中是没有名字的)
- stack叫栈内存,heap叫堆内存
- soldier和A101函数(也是对象)只是临时的关系,他随时可以叫任何名字,它只是存了对象的内存中的地址A101而已
- 如上图所示
prototype和__proto__有什么关系吗?它们存的只是同一个对象的地址而已(不要被名字迷惑,名字都是随便的叫什么都行,你愿意你可以把__proto__叫'共',把prototype叫'共有属性也行')
// 1. 构造函数
// soldier是构造函是因为它返回了新的对象
function soldier(ID){
var obj = {
ID: i,
生命值: 42
}
obj.共 = soldier.原型
return obj
}
// 函数也是对象
// 名字叫soldier.prototype的目的是为了让它看起来和士兵有关联
soldier.原型 = {
兵种:"美国大兵",
攻击力:5,
行走:function(){ /*走俩步的代码*/},
奔跑:function(){ /*狂奔的代码*/ },
死亡:function(){ /*Go die*/ },
攻击:function(){ /*糊他熊脸*/ },
防御:function(){ /*护脸*/ }
}
var soldiers = []
for(var i=0; i<100; i++){
soldiers.push(soldier(i))
}
兵营.batchMake(soldiers)
发明一种语法来分享这种写法 --- 关键字new
new是一个语法糖,可以帮我们写4行代码(隐式的帮我们写),我们看不到这几行代码,但是实际帮我们加上了
只要你在士兵前面使用 new 关键字,那么可以少做四件事情:
-
不用创建临时对象,因为new 会帮你创建(这个临时对象名叫this),this一开始就是一个空对象,我们要做的就是往这个临时对象this上加属性就可以了
-
不用绑定原型,因为 new 会帮你做(new 为了知道原型在哪,所以指定原型的名字为 prototype);
-
不用 return 临时对象,因为 new 会帮你做;
-
不要给原型想名字了,因为 new 指定名字为 prototype。
constructor属性
使用了new之后,就算我们什么也不写,也会有下面这行代码
// 自动生成的
soldier.prototype = {
constructor: soldier
}
为什么要在soldier的原型上加constructor属性呢?
如果我们使用new solder()创建了一个士兵,那么士兵的『创建者』是谁 => 是solder
如果我们使用new solder()创建了100个士兵,那么100士兵的『创建者』是谁 => 还是solder
因此,一开始就有一个共有属性
那就是所有的士兵都是由函数soldier创建出来的
因此,默认属性就有了一个属性"谁创建了我们",也就是soldier,
函数名字是什么那么constructor就是什么
constructor属性为什么会被覆盖掉?
soldier原型上的constructor属性是自动生成的
// 自动生成的
soldier.prototype = {
constructor: soldier
}
如果我们再写一遍soldier.prototype = ... 就会将这个对象覆盖掉,这个对象上就没有了constructor属性
这一点我们可以从内存图中知道:
当我们写了下面的代码,相当于在内存A166中创建了一个对象,并且将soldier.prototype的引用地址由原本的A188改为了A166,那么A188对象也就被任何引用了,而A166中又没有constructor属性,因此就覆盖掉了
soldier.prototype = {
兵种:"美国大兵",
攻击力:5,
行走:function(){ /*走俩步的代码*/},
奔跑:function(){ /*狂奔的代码*/ },
死亡:function(){ /*Go die*/ },
攻击:function(){ /*糊他熊脸*/ },
防御:function(){ /*护脸*/ }
}
备注:
所有的等于号"="都是一样的,都是把右边的值复制到左边
当我们写
soldier.prototype = { constructor: soldier }就是把右边对象的值赋值给左边,对象的值就是内存中的地址(从上面的内存图可以看出也就是A166)
那我们应该怎么写?
第一种方法: 在A166号内存中重写一遍constructor
soldier.prototype = {
constructor: soldier,
兵种:"美国大兵",
攻击力:5,
行走:function(){ /*走俩步的代码*/},
奔跑:function(){ /*狂奔的代码*/ },
死亡:function(){ /*Go die*/ },
攻击:function(){ /*糊他熊脸*/ },
防御:function(){ /*护脸*/ }
}
第二种方法:不要创建A166号对象,将原型上的属性一个个加(推荐这种写法)
soldier.prototype.兵种 = "美国大兵",
soldier.prototype.攻击力 = 5,
soldier.prototype.行走 = function(){ /*走俩步的代码*/},
soldier.prototype.奔跑 = function(){ /*狂奔的代码*/},
soldier.prototype.死亡 = function(){ /*Go die*/ },
soldier.prototype.攻击 = function(){ /*糊他熊脸*/},
soldier.prototype.防御 = function(){ /*护脸*/},
使用new重写后
1. 创建构造函数
function Soldier(name){
// 初始化自有属性
this.ID = i
this.生命值 = 42
this.name = name || '无名战士'
}
// 自动生成:soldier.prototype = {constructor: soldier}
// 初始化共有属性
Soldier.prototype.兵种 = "美国大兵",
Soldier.prototype.攻击力 = 5,
Soldier.prototype.行走 = function(){ /*走俩步的代码*/},
Soldier.prototype.奔跑 = function(){ /*狂奔的代码*/},
Soldier.prototype.死亡 = function(){ /*Go die*/ },
Soldier.prototype.攻击 = function(){ /*糊他熊脸*/},
Soldier.prototype.防御 = function(){ /*护脸*/}
2. 使用构造函数
var soldiers = []
for(var i=0; i<100; i++){
// 等价于 Array.prototype.push.call(soldiers,new Soldier())
soldiers.push(new Soldier())
}
兵营.batchMake(soldiers)
运行的结果:
备注:约定俗成的规定
- 构造函数的首字母大写
- 构造函数的命名可以省略create,因为new就还有create的意思
- 如果构造函数没有参数,可以省略括号
还是想看下使用new后,声明构造函数的时候帮我们加的3句话
function Soldier(name){
// 1.var this = {}
// 2.this.__proto__ = Soldier.prototype
this.ID = i
this.生命值 = 42
this.name = name || '无名战士'
// 3.return this
}
总结:
用了new之后,就是为了创建一个对象,同时指定对象的自有属性和共有属性
总结一下
1.对象与对象之间的关系(原型链)
一个对象通过__proto__(『共』)指向另外一个对象,那么这个对象就又有了另外一个对象的属性
2. 函数和对象的关系(this)
3. 创建对象(new)
new就起到了串联对象和函数的作用
function Soldier(name){
// 初始化自有属性
this.ID = i
this.生命值 = 42
this.name = name || '无名战士'
}
// 自动生成:soldier.prototype = {constructor: soldier}
// 初始化共有属性
Soldier.prototype.兵种 = "美国大兵",
Soldier.prototype.攻击力 = 5,
Soldier.prototype.行走 = function(){ /*走俩步的代码*/},
Soldier.prototype.奔跑 = function(){ /*狂奔的代码*/},
Soldier.prototype.死亡 = function(){ /*Go die*/ },
Soldier.prototype.攻击 = function(){ /*糊他熊脸*/},
Soldier.prototype.防御 = function(){ /*护脸*/}
如上面代码所示:
通过函数Soldier创建对象this
这个对象this,又和函数Soldier的prototype是有关联的通过this.__proto__ = Soldier. prototype
JS中搞来搞去,要么是对象,要么是函数,额外再加上new语法糖帮我们省几行代码
因此,JS这门语言还是相当的“纯粹的”,没有再引入其他复杂的概念
面向对象
概念
它是一种让你在写代码的时候不用再思考如何组织代码的一种思维方式或者是“固定套路”,面向对象的概念很抽象,每个人有不同的理解
JS中的面向对象
1.JS中的面向对象如何实现继承?
通过A对象的__proto__指向另一个对象B就实现了继承
2.JS中如何实现多态?
JS本身就是一种动态语言(自带)
3.JS中如何实现封装
将代码放进一个函数里就实现了封装
如何用JS实现AVA中的继承
JAVA中类的继承
JAVA中的类其实就是JS中的构造函数
但是JAVA中的类和类之间也可以实现继承
例如下图中:在JAVA中只要一个关键字 extend 就可以让“士兵”继承“人类”的属性
用JS模拟JAVA中extend的继承
实现对象与对象共有属性的继承(通过原型链)
var personCommon = {
吃: '',
喝: '',
拉: '',
撒:''
}
undefined
soldierCommon = {
兵种:"美国大兵",
攻击力:5,
行走:function(){ /*走俩步的代码*/},
奔跑:function(){ /*狂奔的代码*/ },
死亡:function(){ /*Go die*/ },
攻击:function(){ /*糊他熊脸*/ },
防御:function(){ /*护脸*/ }
}
{兵种: '美国大兵', 攻击力: 5, 行走: ƒ, 奔跑: ƒ, 死亡: ƒ, …}
soldierCommon.__proto__ = personCommon
{吃: '', 喝: '', 拉: '', 撒: ''}
var s = {}
undefined
s.__proto__ = soldierCommon
{兵种: '美国大兵', 攻击力: 5, 行走: ƒ, 奔跑: ƒ, 死亡: ƒ, …}
s
运行结果: s同时有了士兵的共有属性和人类的共有属性
自有属性怎么办?
自有属性是不能放到原型上的
var personCommon = {
吃: '',
喝: '',
拉: '',
撒:''
}
undefined
soldierCommon = {
兵种:"美国大兵",
攻击力:5,
行走:function(){ /*走俩步的代码*/},
奔跑:function(){ /*狂奔的代码*/ },
死亡:function(){ /*Go die*/ },
攻击:function(){ /*糊他熊脸*/ },
防御:function(){ /*护脸*/ }
}
{兵种: '美国大兵', 攻击力: 5, 行走: ƒ, 奔跑: ƒ, 死亡: ƒ, …}
soldierCommon.__proto__ = personCommon
{吃: '', 喝: '', 拉: '', 撒: ''}
var s = {}
undefined
s.__proto__ = soldierCommon
{兵种: '美国大兵', 攻击力: 5, 行走: ƒ, 奔跑: ƒ, 死亡: ƒ, …}
s.名字 = ''
s.肤色 = ''
s.ID = ''
s.生命值 = '
运行结果: s有了自有属性、士兵的共有属性和人类的共有属性
代码太松散了!
我们需要使用构造函数来做
1. 共有属性的继承
这个简单通过原型就可以实现
function Human(){
this.name = 'xxx',
this.肤色 = 'yyy'
}
Human.prototype.eat = function(){}
Human.prototype.drink = function(){}
Human.prototype.poo = function(){}
function Soldier(){
// 初始化自有属性
this.ID = 100
this.生命值 = 42
this.name = name || '无名战士'
}
// 自动生成:soldier.prototype = {constructor: soldier}
// 初始化共有属性
Soldier.prototype.兵种 = "美国大兵",
Soldier.prototype.攻击力 = 5,
Soldier.prototype.行走 = function(){ /*走俩步的代码*/},
Soldier.prototype.奔跑 = function(){ /*狂奔的代码*/},
Soldier.prototype.死亡 = function(){ /*Go die*/ },
Soldier.prototype.攻击 = function(){ /*糊他熊脸*/},
Soldier.prototype.防御 = function(){ /*护脸*/},
Soldier.prototype.__proto__ = Human.prototype // 共有属性的继承
var s = new Soldier()
运行结果: s有了人类的共有属性
2. 自有属性如何继承?
如何让士兵拥有人类的自有属性:"名字"和"肤色"呢?
如果把Human中的this变成Soldier中的this就好了
于是,我们想到了,不妨在Soldier中使用call调用Human
同时指定Human中的this就是Soldier中的this(this是call的第一个参数)
这样就实现对Human中自有属性的继承
function Human(){
this.name = 'xxx',
this.肤色 = 'yyy'
}
Human.prototype.eat = function(){}
Human.prototype.drink = function(){}
Human.prototype.poo = function(){}
function Soldier(){
// new帮我们加了 this.__proto__ = Soldier.prototype
// 将Human中的this变为Soldier中的this,实现对Human中自有属性的继承
Human.call(this)
// 初始化自有属性
this.ID = 100
this.生命值 = 42
this.name = name || '无名战士'
}
// 自动生成:soldier.prototype = {constructor: soldier}
// 初始化共有属性
Soldier.prototype.兵种 = "美国大兵",
Soldier.prototype.攻击力 = 5,
Soldier.prototype.行走 = function(){ /*走俩步的代码*/},
Soldier.prototype.奔跑 = function(){ /*狂奔的代码*/},
Soldier.prototype.死亡 = function(){ /*Go die*/ },
Soldier.prototype.攻击 = function(){ /*糊他熊脸*/},
Soldier.prototype.防御 = function(){ /*护脸*/},
Soldier.prototype.__proto__ = Human.prototype // 共有属性的继承
var s = new Soldier()
运行结果: s同时拥有了士兵的共有属性和人类的共有属性
这样JS就实现了JAVA中的继承,既实现了共有属性的继承,又实现了自有属性的继承
3.优化下构造函数的参数
function Human(options){
this.name = options.name,
this.肤色 = options.肤色
}
Human.prototype.eat = function(){}
Human.prototype.drink = function(){}
Human.prototype.poo = function(){}
function Soldier(options){
// new帮我们加了 this.__proto__ = Soldier.prototype
// 将Human中的this变为Soldier中的this,实现对Human中自有属性的继承
Human.call(this,options)
// 初始化自有属性
this.ID = options.ID
this.生命值 = 42
}
// 自动生成:soldier.prototype = {constructor: soldier}
// 初始化共有属性
Soldier.prototype.兵种 = "美国大兵",
Soldier.prototype.攻击力 = 5,
Soldier.prototype.行走 = function(){ /*走俩步的代码*/},
Soldier.prototype.奔跑 = function(){ /*狂奔的代码*/},
Soldier.prototype.死亡 = function(){ /*Go die*/ },
Soldier.prototype.攻击 = function(){ /*糊他熊脸*/},
Soldier.prototype.防御 = function(){ /*护脸*/},
Soldier.prototype.__proto__ = Human.prototype // 共有属性的继承
var s = new Soldier({name:'仗剑走天涯',肤色:'黄种',ID:'666'})
运行结果
4. 可是__proto__不能用啊!
Soldier.prototype.__proto__ = Human.prototype 这句话我们不能这样写
需要用其他的办法来模拟它 --- 用new来模拟它
我们不能写__proto__但是new可以!!
那么如何用new实现Soldier.prototype.__proto__ = Human.prototype这句话同样的效果呢?
A:如何让new帮我们运行第二句话?
// new 帮我们在声明构造函数做的三件事
function X(){
// 第一句:this = {}
// 第二句:this.__proto__ = X.prototype
// 第三句:return this
}
B:比较下面2句话在形式上有什么区别:
Soldier.prototype.__proto__ = Human.prototype
this.__proto__ = X.prototype
C:可不可以把Human当做X?
function Human(){
// this = {}
// this.__proto__ = Human.prototype
// return this
}
D: 再比较一下下面这2句话
Soldier.prototype.__proto__ = Human.prototype
this.__proto__ = Human.prototype
左边的this是不是就是Soldier.prototype?
E: 可不可以这样写?
function Human(){
// this = {}
// this.__proto__ = Human.prototype
// return this
}
// 执行new Human()后返回了this
Soldier.prototype = new Human()
===> Soldier.prototype = this
===> Soldier.prototype.__proto__ = this.__proto__
由于Human中`this.__proto__ = Human.prototype`
===> Soldier.prototype.__proto__ = Human.prototype
于是间接的我们就通过下面的代码
function Human(){
// this = {}
// this.__proto__ = Human.prototype
// return this
}
Soldier.prototype = new Human()
实现了Soldier.prototype.__proto__ = Human.prototype
F: 但是我们不能这样写Soldier.prototype = new Human()
因为我们需要额外的再声明一次空的Human,我们写的代码中就已经声明了一个Human,而且这个Human不是空的,它里面有2个自有属性“name”和“肤色”,如果这样写那么Soldier.prototype中就多了2个自有属性“name”和“肤色”
也就是说得需要一个空函数才能模拟出Soldier.prototype.__proto__ = Human.prototype
G: 如何用空函数模拟出Soldier.prototype.__proto__ = Human.prototype
function fakeHuman(){
// this = {}
// this.__proto__ = fakeHuman.prototype
// return this
}
fakeHuman.prototype = Human.prototype // 相当于把Human的共有属性拿过来,Human的自有属性不要(name和肤色)
Soldier.prototype = new fakeHuman()
===> Soldier.prototype.__proto__ === fakeHuman.prototype === Human.prototype
===> Soldier.prototype.__proto__ === Human.prototype
H. 兼容ie的方式实现的继承
也就是说我们用下面的三句话,模拟出了Soldier.prototype.__proto__ === Human.prototype
function fakeHuman(){}
fakeHuman.prototype = Human.prototype
Soldier.prototype = new fakeHuman()
一定一定要花时间去理解上面3句话!!!!!这是兼容IE的古老的继承的写法
看下完整的代码
function Human(options){
this.name = options.name,
this.肤色 = options.肤色
}
Human.prototype.eat = function(){}
Human.prototype.drink = function(){}
Human.prototype.poo = function(){}
function Soldier(options){
// new帮我们加了 this.__proto__ = Soldier.prototype
// 将Human中的this变为Soldier中的this,实现对Human中自有属性的继承
Human.call(this,options)
// 初始化自有属性
this.ID = options.ID
this.生命值 = 42
}
// 自动生成:soldier.prototype = {constructor: soldier}
// Soldier.prototype.__proto__ = Human.prototype 不能写__proto__
// 用下面的3行代码模拟 Soldier.prototype.__proto__ = Human.prototype
// 要放在Soldier.prototype之前否则Soldier.prototype上的属性就被覆盖了
function fakeHuman(){}
fakeHuman.prototype = Human.prototype
Soldier.prototype = new fakeHuman()
// 初始化共有属性
Soldier.prototype.兵种 = "美国大兵",
Soldier.prototype.攻击力 = 5,
Soldier.prototype.行走 = function(){ /*走俩步的代码*/},
Soldier.prototype.奔跑 = function(){ /*狂奔的代码*/},
Soldier.prototype.死亡 = function(){ /*Go die*/ },
Soldier.prototype.攻击 = function(){ /*糊他熊脸*/},
Soldier.prototype.防御 = function(){ /*护脸*/}
var s = new Soldier({name:'仗剑走天涯',肤色:'黄种',ID:'666'})
运行结果:
提示:
-
每个对象都有__proto__ ( 除了 var obj = Object.create(null) )
-
每个函数对象都有 prototype。这个属性用于实现“实例化”(函数对象也是对象所以也有__proto__,即
Func.__proto__ = Function.prototype) -
函数对象的 prototype 所指向的也是对象,所以也有
__proto__,即Func.prototype.__proto__。这个属性用于实现“继承” -
如果 h = new Human() 那么
h.__proto__ = Human.prototype,因为如果h的构建者是Human,那么它的.__proto__就是指向Human的原型
I. 后续的一种新的写法 -- Object.create()实现继承
代码 Soldier.prototype.__proto__ = Human.prototype
等价于
// 兼容ie的写法
function fakeHuman(){}
fakeHuman.prototype = Human.prototype
Soldier.prototype = new fakeHuman()
也等价于:
// 不兼容ie的写法
Soldier.prototype = Object.create(Human.prototype)
它的意思是让Soldier原型对象的__proto__指向Human的原型对象
完整代码:
function Human(options){
this.name = options.name,
this.肤色 = options.肤色
}
Human.prototype.eat = function(){}
Human.prototype.drink = function(){}
Human.prototype.poo = function(){}
function Soldier(options){
// new帮我们加了 this.__proto__ = Soldier.prototype
// 将Human中的this变为Soldier中的this,实现对Human中自有属性的继承
Human.call(this,options)
// 初始化自有属性
this.ID = options.ID
this.生命值 = 42
}
// 自动生成:soldier.prototype = {constructor: soldier}
// Soldier.prototype.__proto__ = Human.prototype 不能写__proto__
// 用下面的代码模拟 Soldier.prototype.__proto__ = Human.prototype
Soldier.prototype = Object.create(Human.prototype)
// 初始化共有属性
Soldier.prototype.兵种 = "美国大兵",
Soldier.prototype.攻击力 = 5,
Soldier.prototype.行走 = function(){ /*走俩步的代码*/},
Soldier.prototype.奔跑 = function(){ /*狂奔的代码*/},
Soldier.prototype.死亡 = function(){ /*Go die*/ },
Soldier.prototype.攻击 = function(){ /*糊他熊脸*/},
Soldier.prototype.防御 = function(){ /*护脸*/}
var s = new Soldier({name:'仗剑走天涯',肤色:'黄种',ID:'666'})
运行结果:
用class实现继承
非class方式的继承:
function Human(options){
this.name = options.name,
this.肤色 = options.肤色
}
Human.prototype.eat = function(){}
Human.prototype.drink = function(){}
Human.prototype.poo = function(){}
function Soldier(options){
Human.call(this,options)
this.ID = options.ID
this.生命值 = 42
}
Soldier.prototype = Object.create(Human.prototype)
Soldier.prototype.兵种 = "美国大兵",
Soldier.prototype.攻击力 = 5,
Soldier.prototype.行走 = function(){ /*走俩步的代码*/},
Soldier.prototype.奔跑 = function(){ /*狂奔的代码*/},
Soldier.prototype.死亡 = function(){ /*Go die*/ },
Soldier.prototype.攻击 = function(){ /*糊他熊脸*/},
Soldier.prototype.防御 = function(){ /*护脸*/}
var s = new Soldier({name:'仗剑走天涯',肤色:'黄种',ID:'666'})
class方式的继承:
把上面的代码挪挪位置,变成新的语法,就改写成了class的方式,继承的逻辑还是一样,只是『新瓶装旧酒』
class Human{
constructor(options){
this.name = options.name,
this.肤色 = options.肤色
}
eat(){}
drink(){}
poo(){}
}
class Soldier extends Human{ // extends相当于帮我们写了Soldier.prototype = Object.create(Human.prototype)
constructor(options){
super(options) // 这句话的意思就是把Human中的this变为Soldier中的this,这样Sildier也就有了name和肤色
this.ID = options.ID
this.生命值 = 42
// class写法中不支持非函数属性放到原型上
this.兵种 = "美国大兵"
this.攻击力 = 5
}
行走(){ /*走俩步的代码*/}
奔跑(){ /*狂奔的代码*/}
死亡(){ /*Go die*/ }
攻击(){ /*糊他熊脸*/}
防御(){ /*护脸*/}
}
var s = new Soldier({name:'仗剑走天涯',肤色:'黄种',ID:'666'})
运行结果:
class的写法确实变简洁了,但是并没有变得简单,JS中还是用原型链实现继承比较简单容易理解
备注:
- 把所有的自有属性放到
constructor里面,把共有属性放到constructor外面 extends对共有属性进行继承super()是对共有属性的继承
隐藏的bug
1.假的class
class Human(){}中的Human的类型是什么?
是类吗?不是!它还是函数
也就是说看起来像是class,实际上还是用原型链模拟了class
2. class中的共有属性不能是非函数
class Soldier extends Human{
constructor(options){
super(options)
this.ID = options.ID
this.生命值 = 42
// 共有属性不能是非函数
this.兵种 = "美国大兵"
this.攻击力 = 5
}
行走(){ /*走俩步的代码*/}
奔跑(){ /*狂奔的代码*/}
死亡(){ /*Go die*/ }
攻击(){ /*糊他熊脸*/}
防御(){ /*护脸*/}
}