文章目录
JavaScript毕竟不是Java,它也不是纯粹的面向对象语言,我的这篇博文只是模拟相关面向对象编程。同时也是深入学习JavaScript,将它与Java思想结合遇到的一些问题,在这里进行总结。
同时,最新的es6已经有class,extends等关键字了,这篇博文不考虑也不讲。我们就硬写JS代码去模仿面向对象,这样理解更深。
耐心看下去,相信你一定能有所收获。
一、JavaScript模拟面向对象
1、函数是类
function Obj() { // <=> var Obj = function(){}
};
var o1 = new Obj(); //<=> var o1 = new Obj;
console.log("o1:",o1);
console.log("o1 instanceof Obj",o1 instanceof Obj);//true
var o2 = new Obj;
console.log("o1 == o2",o1 == o2); //false
console.log("o2 instanceof Obj", o2 instanceof Obj);//true
上述想说明function是一个类,因为它可以用new产生自己的对象。
o1、o2是Obj的一个实例化对象,而Obj已经可以抽象成类的概念了。
2、函数中各种变量的声明
var Fun = function () { //类名称首字母大写,遵循Java的原则吧
varOne = 100;
var varTwo = 200;
this.varThree = 300;
console.log(window);
}
var fun1 = new Fun();
fun1.varThree = "abc";
console.log("fun1", fun1);
var fun2 = new Fun;
console.log("fun2", fun2);
这里有window的输出,说明Fun函数被调用了。也同时可以类比无参构造。也就说明,function是函数,是类,也是类的构造方法。
出现 Fun,varOne,fun1和fun2,给window加的键值对(成员)。在JS里,只要被称为对象的,都可以看作是一个Map,里面可以有键值对。这样看来window也是一个对象。
1.函数内无任何前缀变量
在一个函数里面直接写一个没有任何前缀的变量,其前缀默认是window,相当于varOne <=> window.varOne,给window对象加键值对(成员)。window也是个对象,它再大也是个对象,里面由很多键值对组成的。所以以前称它为全部变量是错误的,它只是window对象底下的一个键值对而已。
2.函数内定义var变量
函数内定义的var varTwo = 200; 是函数的局部变量,将随着函数运行结束而释放,用的是堆栈空间。局部变量用的是系统堆栈,随着函数运行结束,栈顶指针下移,局部变量用的堆栈空间被释放了。对所有语言来说,局部变量都是如此。
3.函数内加this修饰的变量
例如this.varThree = 300。varThree成了对象fun1和fun2的(成员)键值对。更改fun1对象的varThree的值,而fun2的不会改还是函数执行(构造方法)出来的。且,这类成员是public的,或者说,模拟public成员。就像Java里面类的成员一样,每一个该类的对象都拥有自己的成员!只不过和Java不一样的地方,Java需要前面给确定的类型,但JavaScript是弱类型,没有这么一说。同时Java成员还可以有修饰符。
3、关于函数内的this
参照Java中的this,在Java中new的时候会调用构造方法,构造方法被对象调用,这个this其实就是那个调用构造方法的对象。照搬到JavaScript中也可以解释通。JavaScript中调用函数后,函数里的this自动被替换为调用该函数的对象。
小结:JavaScript中函数是什么?
前期小结:在JavaScript中
- 函数是函数:可以调用的,直接写在script标签下是window对象的一个键值对。
- 函数也是类:参考Java的思想,可以实例化对象出来。
- 函数也是类的构造函数:在new对象时被自动执行
- 函数还是对象:可以有自己的键值对
在JavaScript可以类比Java记住:对象(map)是由一些键值对(成员)组成的。
4、练习:面向对象思想编写Complex类
又感觉回到学习Java的第一节课编写Complex类。速速写出。
var Complex = function () {
this.real = 0.0;
this.vir = 0.0;
this.setReal = function (real) {
this.real = real;
}
this.setVir = function (vir) {
this.vir = vir;
}
this.getReal = function () {
// return real;
return this.real; //注意这里return的是this.real。因为可能出现window底下也有real这个键值对错误!!!
}
this.getVir = function () {
return this.vir;
}
this.toString = function () {
return "(" + this.real + ", " + this.vir +")"
}
}
var complexOne = new Complex();
complexOne.setReal(1.2);
complexOne.setVir(9.8);
console.log("complexOne:", "" + complexOne);
需要注意的点时如果要获得成员的值,一定要记得加this修饰,否则可能会获取widow下同名的值,没达到预期目标。
5、模拟静态成员初尝试
我们知道Java中需要一种属于整个类而不独属于某个实例对象的成员称为静态变量。JavaScript可以实现吗?因为没有修饰符static
这种关键字,那么通过类.名称
可以吗?尝试下。
var Complex = function () {
// Complex.staticField = 5; 不建议在这里写,因为每次new都会给这个重新赋值
}
Complex.staticField = 5; //建议写在Complex函数下面外部。
var complexOne = new Complex();
console.log(complexOne.staticField); //undefined
结果为undefined
。说明未定义,没有办法访问到。这不是静态成员。仔细分析结合上文我们得知原因:我们知道Complex是个类,但它也是个对象,上述操作只相当于给Complex这个对象增加个键值对,而不是给它实例化对象增加的。
那么JavaScript中的静态成员应该如何定义呢?这个问题我们先放一放,因为它要牵扯到一个重要的JavaScript中的原型(Prototype)的问题!
6、初探究prototype原型和__proto__原型链
在JavaScript中,所有的”类“(函数)都存在”原型“,型是类型的意思;原是说明的意思,有点meta的意思。
这个meta往Java方向想,例如String.class
和Complex.class
,这个class是所有类都有的一个单例的、静态的元数据。元数据是解释数据的数据,我们的反射机制就是基于这个class而来的,class描述这个类有哪些成员,哪些方法什么的。
我们经常锁的就是这个class。因为多个同一个类实例化的对象需要找到一个他们都认识的锁。类.class是对象都认识的,所以用它来保证多线程安全问题。
synchronized (Complex.class) {
}
JavaScript的类都有一个prototype(原型);JavaScript的对象(函数除外)都绝对不会存在prototype!这就是区分类和对象的方法!即,如果想区分一个对象是否也是类,则,查看其是否存在prototype成员!若存在,则它既是对象,也是类;若不存在(undefined),则它仅仅是对象!
在JavaScript里所有的对象一定都会有__proto__
(前后各两个下划线),火狐浏览器为<property>
(不管它,别记这个,易混淆),这个就是原型链,以后我们都称__proto__
。
6.1、JavaScript处处是对象
在Java中我们就经常说,Java处处是对象,Java没有指针,快都来学Java简单易上手!并且,我可以很负责任的说,JavaScript才能被称为处处是对象!因为它你几乎看到的所有数据都可以用对象表示出来。同时当你深入学习理解后,才会发现没有指针这句话是骗人的。
指针,学C的时候一生的痛。当初上课这里就没好好听,下面做练习更头晕,见了指针这个词我就PTSD。然后学了这么久Java,以为终于逃脱了指针的魔爪。但接下来我要在你耳边进行恶魔的低语:“对象就是指针,指针就是对象!”是的,使我一直头疼的指针原来就在身边,只不过改了名而已,所以好受点。解释也十分简单,听了这个解释你以后见了指针(对象)都不会怕。“指针就是内存中的地址值!”,这就是指针的本质。
这样想一切就通了。你new出来的是对象吧,new出来对象空间是在哪申请呢?堆空间。实际上new出来的对象需要一大段空间放自己的成员方法等数据,这一大段数据在堆栈空间。而堆空间(动态存储)常常放的就是一些你看不懂的(通常为四字节)十六进制码(或者C中输出指针),这个就是内存中的地址值。此地址值就是那个新开辟的堆栈空间的首地址。所以我说对象就是指针,指针就是对象!一切别离其宗,指针就是内存中的地址值。
function Fun(real, vir) {
real = real || 0;
vir = vir || 0;
this.real = real;
this.vir = vir;
}
var fun1 = new Fun(9.8,-1.2);
console.log("fun1 instanceof Fun:",fun1 instanceof Fun);//true
var fun2 = new Fun(5.6);
console.log("fun2 instanceof Fun",fun2 instanceof Fun);//true
console.log("Fun:", Fun);
console.log("fun1", fun1);
前面我们得到的结论在JavaScript中每个类都有prototype(原型),每个对象都有__proto__
(原型链)。这种说法还是不够准确,直接引入概念可能会让人理解上有些困难。应该看其本质。
我们可以看到将类进行展开,prototype(原型)展开里面还有__proto__
,所以说明prototype(原型)本身也是一个对象。并且__proto__
展开也有__proto__
,说明原型链也是个对象。
所以,我们定义一个准确的结论。在JavaScript中,所有的类都有prototype(原型对象),每个对象都有__proto__
(原型链对象)对象就是指针,对象就是一个存在于堆空间的地址值。
7、instanceof的判断依据
function Fun(real, vir) {
real = real || 0;
vir = vir || 0;
this.real = real;
this.vir = vir;
}
var fun1 = new Fun(9.8,-1.2);
var fun2 = new Fun(5.6);
console.log("fun1.__proto__ === Fun.prototype:", fun1.__proto__ === Fun.prototype);//true
//反证法
function ABC() {
}
fun2.__proto__ = ABC.prototype;
console.log("fun2.__proto__ === Fun.prototype:", fun2.__proto__ === Fun.prototype);//false
console.log("fun2.__proto__ === ABC.prototype:", fun2.__proto__ === ABC.prototype);//true
fun1是由Fun实例化出来的,任何对象都有原型链对象,任何类都有原型对象。第一个输出正面验证了一个对象的原型链对象指向其所属类的原型对象。
再看反证法。fun2也是Fun实例化出来,只不过它改了原型链对象的值为另一个类的原型对象,结果输出就不同了。
结合以上两点,我们可以得出instanceof关键字判断原则:一个对象的原型链对象是否指向(相等)那个类的原型对象。简而言之,对象的原型链对象与类的原型对象是否指向同一空间。
8、模拟静态成员
function Fun(real, vir) {
real = real || 0;
vir = vir || 0;
this.real = real;
this.vir = vir;
}
var fun1 = new Fun(9.8,-1.2);
var fun2 = new Fun(5.6);
Fun.prototype.count = 10;
console.log(fun1.count);
console.log(fun2.count);
我们给类的原型对象增加count成员。fun1和fun2都没有增加count成员,但是却可以输出。这个输出怎么得到的,这点必须要深入思考,用图来描述更加形象。
8.1、图解关于prototype(原型对象)和__proto__(原型链对象)
画出上述代码的结构图。
可以看到Fun类是由一些键值对和prototype(原型对象)和__proto__
(原型链对象)组成的。而两个实例化对象由键值对和__proto__
(原型链对象)组成的。且按照前面的结论,实例化对象的__proto__
与类的prototype指向同一空间(指针值相同)。
从中不难理解出count的值是通过实例化对象的原型链对象来的。所以,可以总结__proto__
(原型链对象)作用。
__proto__
(原型链对象)作用
- 表示一个对象其所从属的类(instanceof)。
- 在"对象.成员"访问失败时,会沿着原型链继续访问。
8.2、左值问题
function Fun(real, vir) {
real = real || 0;
vir = vir || 0;
this.real = real;
this.vir = vir;
}
var fun1 = new Fun(9.8,-1.2);
var fun2 = new Fun(5.6);
Fun.prototype.count = 10;
console.log("fun1.count", fun1.count);
console.log("fun2.count", fun2.count);
fun1.count = "abc";
console.log("再次输出fun1.count", fun1.count);
console.log("再次输出fun2.count", fun2.count);
console.log("fun1", fun1);
console.log("fun2", fun2);
我们仅仅只加fun1.count = “abc”;这一句话,输出就完全不同。
仔细观察输出结果,可以发现此时fun1有两个count,一个是自己的键值对count,一个是原型链中的count,这就牵扯一个十分重要的问题,左值和非左值问题。
所谓左值:在赋值语句中,=左侧的变量。非左值即除了左值外的其他任何场合的变量(直接参加输出或运算的)。
JavaScript中对左值的处理原则:
- 查看该左值是否存在;
- 若不存在,则创建;
- 完成赋值操作。
fun1.count = "abc";
因为fun1.count作为左值出现,因此,先在fun1中找count,由于其不存在,因此给fun1增加一个键:count,然后再赋值。
console.log("再次输出fun1.count", fun1.count);
因为fun1.count作为非左值出现,因此,在fun1中找count,找到了,并输出。
console.log("再次输出fun2.count", fun2.count);
因为fun2.count作为非左值出现,因此,在fun2中找count,未找到,则继续通过原型链对象查找,结果在类原型对象找到了;若还是未找到则返回undefined。
8.3、模拟静态成员的规定
经过上述问题探究。对于类的原型对象中产生的“静态成员(模拟)”,其访问方式必须是:类.prototype.成员。这类似于Java中通过类.静态成员进行访问。
即应该这样访问。
Fun.prototype.count = 98;
这种访问方式和对象无关,这也真正的表现出了它是静态成员这么一种特性(属于整个类不属于某一个实例化对象)。虽然书写很长,但是这样写语义上可以看出是与实例化对象无关的静态成员。
9、模拟继承
在JavaScript中,如果想做到两个类有继承关系,只需要做到如下图所示的关系。
即,如果Chlid类的原型对象的原型链,指向Parent类的原型对象,那么,就可以说Child是Parent的派生类。
var Parent = function () {
this.parentMem = 10;
};
var Child = function () {
this.childrenMem = 10;
};
Child.prototype.__proto__ = Parent.prototype;
var obj1 = new Parent();
var obj2 = new Child();
console.log("obj1 instanceof Parent:", obj1 instanceof Parent);//true
console.log("obj2 instanceof Child:", obj2 instanceof Child);//true
console.log("obj2 instanceof Parent:", obj2 instanceof Parent);//true
console.log(obj2)
使用Child.prototype.__proto__ = Parent.prototype;
完成了两个类的继承。但是不能使用这种简单直白的实现两个类的继承关系。原因如下。
在几乎所有程序设计语言中,用下划线开头的变量,几乎都是被这个语言“隐藏”起来的变量;即,这个语言极其不建议编程者直接操作这类变量。(这就是为什么Firefox将__proto__
隐藏起来,而显示为<prototype>
的原因)。
不建议使用__proto__
,那我们可以借助中间变量为Chlid类的原型对象进行赋值。
var Parent = function () {
this.parentMember = 10;
};
var Child = function () {
this.childrenMember = 10;
};
function Extend(parent, child) {
if(parent === undefined || child === undefined
|| parent.prototype === undefined
|| child.prototype === undefined) {
return;
}
var object = new parent; //根据父类产生个实例化对象
object.constructor = Child; //类名就是它的构造方法
child.prototype = object; //修改child的原型对象
};
Extend(Parent, Child);
var obj1 = new Parent();
var obj2 = new Child();
console.log("obj1 instanceof Parent:", obj1 instanceof Parent);//true
console.log("obj2 instanceof Child:", obj2 instanceof Child);//true
console.log("obj2 instanceof Parent:", obj2 instanceof Parent);//true
console.log(obj2);
完成类的继承,其实就是想改一下child的原型对象,改成parent的原型对象。借助个parent实例化出来的对象的原型链是指向parent的原型对象的,只需要再将相应的constructor改成Child的就可以了,相当于给child换了个指向关系正确的原型对象。
9.1、子类继承父类成员
上述代码还是存在一个问题,就是实例化出来的对象obj2依然没有Parent类的成员parentMember。这不符合Java的继承性。所以,继续进行修改。
问题的原因是什么呢?我们上述操作只是对Child和Parent进行原型对象和原型链上的修改,可以让Child实例化的对象通过原型链找到Parent(所以instanceof成功了)。但是相关构造方法没执行啊!你仅仅只是var obj2 = new Child()
调用了Child的构造方法,Parent的构造方法却没调用。所以我们需要在Child的构造方法里面加Parent的构造方法的执行(就像Java中子类构造方法必会调用super()
那样)。
var Parent = function () {
this.parentMember = 10;
};
var Child = function () {
//Parent();一开始直接写的这个。
Parent.call(this);
this.childrenMember = 10;
};
function Extend(parent, child) {
if(parent === undefined || child === undefined
|| parent.prototype === undefined
|| child.prototype === undefined) {
return;
}
var object = new parent;
object.constructor = Child;
child.prototype = object;
};
Extend(Parent, Child);
var obj1 = new Parent();
var obj2 = new Child();
console.log("obj1 instanceof Parent:", obj1 instanceof Parent);
console.log("obj2 instanceof Child:", obj2 instanceof Child);
console.log("obj2 instanceof Parent:", obj2 instanceof Parent);
console.log(obj2);
需要调用父类的构造方法。我一开始直接写的Parent(),进行直接调用,但还是没达到预期目标。然后就在这里思考哪里出错了。其实这又回到了基础知识。在一个函数里面直接写一个没有任何前缀的变量,其前缀默认是window,因此直接写Parent(),相当于window.Parent()。这是不对的,我们需要为当前new的对象执行该构造方法,所以需要借助到call()函数!
call()函数可以更改所执行函数的对象,即谁来执行这个函数。
call()用法格式。 哪个方法.call(哪个对象执行)
总结,为了实现两个类的继承可以使用如下方式。
//1.调用这个写好的函数
function Extend(parent, child) {
if(parent === undefined || child === undefined
|| parent.prototype === undefined
|| child.prototype === undefined) {
return;
}
var object = new parent;
object.constructor = Child;
child.prototype = object;
};
//2.子类构造函数需进行 父类.call(this);
二、一张图帮助你深刻理解原型链和原型对象
首先要接受前文总结的定理。在JavaScript中,一个函数就是一个类,也是一个对象。所有函数(类)中必定存在一个prototype(原型对象),所有对象必定存在一个__proto__
(原型链对象)。对象就是指针的概念。
然后,就可以描述我接下来画的图你需要看懂的两个基本组成部分了。
我们任意定义一个函数(类)
var Func = function () {
this.FuncMember = 10;
}
console.log("Func:", Func);
其可以转换为我们最基本的两个部分图。
图左为在JavaScript中任意定义一个函数(类)的组成部分的映像图,一些键值对是该函数(类)的参数名字等信息,然后就是我们最重要需要分析的原型对象和原型链对象。图右为该函数(类)的真正的原型对象的空间。原型对象只是个指针,其指向的才是真正的空间。该空间存在键值对和我们所要重点关注的原型链对象。
注:键值对可能截图不完整,可以自行在控制台下输出查看。
其中JavaScript代码是
var Func = function () { //任意定义一个函数
this.FuncMember = 10;
}
console.log("Func:", Func); //输出有__ptoto__,说明也是个对象,是谁的对象呢?
console.log("Function:",Function);//输出下大写的Function,有原型对象,说明是个类
console.log("Func instanceof Function:",Func instanceof Function);//true,说明js中任意函数(类)是Function的对象
console.log("Func.__proto__ === Function.prototype:", Func.__proto__ === Function.prototype);//todo 画线一
//1.前面说过,instanceof判断标准一个对象的原型链是否是一个类的原型对象相同(指向同一空间)证实了一个问题,在js中任何函数(类)都是Function的对象
console.log("Function.__proto__ === Function.prototype:",Function.__proto__ === Function.prototype);//todo 画线二
//2.true。Function既是类,也是他自身的对象 在控制台展开他,他就是无限循环
//从1、2总结 所有函数(包括Function)的原型链对象都指向Function的原型对象
console.log("----------------------------------------------------");
var funcObj = new Func(); //定义Func的一个实例化对象
console.log("Object:",Object);
console.log("funcObj:", funcObj);//有FuncMember成员(键值对)
console.log("funcObj.__proto__ === Func.prototype:", funcObj.__proto__ === Func.prototype); //todo 画线三
//true。说明对象的原型链对象和其所属类(父类)的原型对象相同。
//既然Func函数的原型对象也是个对象,那么这个函数对象的原型链对象指向哪呢?
console.log("Func.prototype.__proto__ === Object.prototype", Func.prototype.__proto__ === Object.prototype);//todo 画线四
//true。其指向Object函数(类)的原型对象。验证了所有对象的原型链对象指向其父类的原型对象。
console.log("Function.prototype.__proto__ === Object.prototype:", Function.prototype.__proto__ === Object.prototype);//todo 画线五
//true。再次验证了所有对象的原型链对象指向其父类的原型对象。Object是所有类的基类,所以最终回到Object。
//那么Object的原型对象指向谁呢?
console.log("Object.prototype.__proto__:", Object.prototype.__proto__);
//为null,终结
//做到这里就要自己有主观能动性画图了。
console.log("Object.__proto__ === Function.prototype:", Object.__proto__ === Function.prototype);//todo 画线六
//为true 完全不出乎意料。Object常用作类,类就是函数,前面我们说了只要是在js中任意定义的函数,那都是Function的对象。所以这里为true。
console.log("----------------------------------------------------");
//接下来我们自己定义有继承关系的两个类,然后再去画映像图,用我们前面的知识构造继承关系
var Parent = function () {
this.parentMember = 20;
}
var Child = function () {
Parent.call(this);
this.childMember = 30;
}
function Extend(parent, child) {
var object = new parent;
object.constructor = Child;
child.prototype = object;
};
Extend(Parent, Child);
console.log("Parent:", Parent);
console.log("Child:", Child);
console.log(Child.__proto__ === Function.prototype);
console.log(Child.prototype.__proto__ === Parent.prototype);
console.log(Parent.__proto__ === Function.prototype);
console.log(Parent.prototype.__proto__ === Object.prototype);
//都是true 可以总结结论了。
图像是:
结论。
- 在JavaScript中任何函数都是Function的对象。Function既是类,也是他自己的对象。在控制台展开它,它是个无限循环。
- 所有函数的原型链(包括Functon的原型链)都指向Function的原型对象。也就是说,Function也是对象,其类是自己。
- JavaScript中任意的对象都有原型链对象,其都指向其所属类的原型对象(最基础,也是定理)!所有类都有原型对象,原型对象也是个对象,其所指向为“父类”的原型对象。
- JavaScript中所有对象都存在一个原型链,其都指向其所属类的原型对象;所有类的原型对象本身是对象,其原型链指向“父类”的原型对象;直到最顶层类的原型对象的原型链,指向Object类的原型对象。
上面结论文字纯属我看着图像随手叙述的,没有进行什么规整。所以如果你一直看就会看晕,但是如果你仔细一步步来画出图像,就能清楚地理解原型对象和原型链的知识。
然后我们回头再看最基本的两个部分的映像图(我为什么基本元素要这样画的原因)。
左边是函数(类),右边是原型对象实例(对象)。
左边是类(最终走向Function),右边是对象(最终走向Object)。
然后注意函数会在Function那边一直绕,而所有的对象这边最终走到Object这里,再往下走就是null了。
手画映像图不易,望一件三连。