a.本文所有代码在chrome浏览器下测试通过,建议读者也安装一个chrome浏览器;
b.本文所述遵循ECMA-262第五版规范
c.本文输出使用console.log函数,请按F12进入调试模式,观看控制台输出;
d.源码链接地址e.转载请注明出处.
1.什么是对象?
javascript本身,是没有类的概念的,只有对象的概念,除了基本类型(string,number,boolean,null,undefined)外,其余均是对象,就连function也是对象.那么,什么是对象?!javascript中的对象,类似于一组键值对的集合.你甚至可以以键值对的方式来操作javascript中的对象,就像这样:
var myDog= new Object();
myDog["name"] = "Odie";
myDog["color"] = "Yellow";
console.log(myDog ["name"] );
console.log(myDog ["color"] );
跟这种方式创建对象的效果是一样的:
var myDog = new Object();
myDog .name = "Odie";
myDog .color = "Yellow";
console.log(myDog.name );
console.log(myDog.color );
显然,第二种访问方式更加方便.通常只有在并不确定我们访问的对象的属性名字的时候(比如json数据),才会使用这种方式访问.当然,如果你想起一个类似"hello world"(有空格)这样奇葩的属性名字,那你就必须使用键值对的方式创建对象啦.
2.如何创建对象?
exampleA:
//字面量方式创建对象,简单方便,可以认为,这是exampleB方式的简写
var myDog= {name: "Odie", color: "Yellow"};
exampleB:
//使用new操作符创建对象,再追加属性
var myDog= new Object();
myDog.name = "Odie";
myDog.color = "Yellow";
在js中,如果你赋值操作的属性没有,就会创建一个,1话题中就已经用过这种方式了.exampleA中自然也可以这样继续追加属性,A和B最大的区别在于创建对象的方式(主要区别在第一行),而不在于追加属性上.值得注意是只有在赋值操作时,才会这样,你做一个访问的操作,它自然是不会创建的(记住这一点,在后面的话题中很重要).
简单验证一下:
var myDog= {name: "Odie", color: "Yellow"};
console.log(myDog.age);//undefined
for(var pro in myDog){console.log(pro);}//name,color,没有age
exampleC:
//一个很少使用的方式
var myDog= Object.create(new Object());
myDog.name = "Odie";
myDog.color = "Yellow";
这种方式着实很少用,它有什么用途在这里不好讲,留到后面吧.
其实A、B、C三种方式创建对象还是有差别的,在这里也不好说,也留在后面吧.
exampleD:
//如果是简单的对象,似乎exampleA是最方便的,但当你要创建复杂的对象,亦或是大量类似的对象时,就应当考虑下面这个方法了.
function Dog(name, color) {
this.name = name;this.color = color;
}
var myDog= new Dog("Odie", "Yellow");
等等,为什么new了一个function呢?!学过其他OO语言的人都会觉的怪怪的.之前说过,javascript根本就没有类这个概念,我们输出一下浏览器内置的Object和Date:
console.log(Object);
console.log(Date);
在chrome中的结果:
function Object() { [native code] }
function Date() { [native code] }
在firefox中的结果:
[object Function]
[object Function]
真相了,Object和Date也不是所谓的类,而是function!在javascript中,就是使用"new function(参数)"这种方式创建对象的.我们称这种function为构造函数,exampleB和exampleD是同一种方式,只是在exampleD中,使用了自己定义的构造函数.
那么在 "new 构造函数(参数)" 的过程中,都发生了什么呢?!
1.创建一个新对象//对象的隐性引用"__proto__"指向构造函数的"prototype"(先忽略后面这半句);
2.将构造函数的 "this" 指向新对象;
3.执行构造函数(初始化,向新对象添加属性);
4.返回这个对象.
经过以上步骤,一个新的对象就创建出来了.
构造函数与其他函数有什么区别么?!
唯一区别是你在调用构造函数上需要使用new关键字,但在语法上并没有区别,所有的函数都有prototype和this引用,构造函数只是定义的时候通常首字母大写,调用的时候记得使用new关键字,但这都靠自觉,javascript本身没有语法强制规定什么样的函数才是构造函数.你不用new关键字调用构造函数(非strict mode)亦或你new一个普通的函数也不会报错,当然,结果自然不是你想要的(后面会讲会发生什么).
this是什么东西?!
this是function内部的一个引用,跟其他OO语言类似,它指向函数据以执行的对象.所有的函数都有这么一个引用.
什么叫据以执行的对象?!
不太好形容这个东西,大概可以理解为,这个函数运行在哪个对象之下(还是不大好理解),直接举例吧.
//输出自己的this function log_this() { console.log(this); } //初始化 window.onload = function() { //example_one log_division("example_one"); log_this();//window对象 //example_two假设你有一个btn按钮 document.getElementById("btn").onclick = log_this;//点击-->dom对象 //example_three log_division("example_three"); var temp = {};//exampleA中创建对象的方法哦 temp.logThis = log_this;//function也是对象,可以这么给对象添加function属性,上面那个onclick监听回调函数的原理,跟这个类似 temp.logThis();//Object }这回好理解了吧,this指向哪里,是根据运行环境有关的,this指向它的运行环境对象,one例子当中,它就指向了window,two中就指向了dom对象,three中就指向了自定义对象.因此,如果你不用new调用构造函数,就会将属性添加到window上.//题外话,在strict mode下,这么做会报错的.
如何定义对象的函数呢?!
你可以这样:
function Dog(name, color) { this.name = name; this.color = color; this.sayName = function(){ console.log(this.name); }; }
前文说过,function也是对象,这就相当于在执行构造函数的过程中,给对象增加了一个类型为function的sayName属性,但这样做有一个问题,就是每次执行构造函数的过程中,都会创建一个新的函数对象,显然,所有的Dog对象共享一个sayName函数就可以了.按照目前所讲的知识,你自然可以这样:
function Dog(name, color) { this.name = name; this.color = color; this.sayName = sayName; } function sayName() { console.log(this.name); }
在这里,定义了一个sayName的全局函数,这样,每次执行构造函数时,就不会重新创建一个新的sayName函数了.但这样做,想给对象定义多个函数,就要定义多个的全局函数.而当你想要定义多种对象时,全局函数的数量就会爆炸,变的让你无法掌控.
3.prototype!!!!!
幸好,可以使用函数的prototype这个对象,看下面的代码:
function Dog(name, color) { this.name = name; this.color = color; } Dog.prototype.sayName = function() { console.log(this.name); }; var myDog = new Dog("Odie", "Yellow"); myDog.sayName();//Odie
再次强调,function也是对象(除了基本类型都是对象).在javascript中,每个function都有一个prototype对象.那么,"Dog.prototype.sayName = function(){...};"这种写法就是给Dog的ptototype增加一个名字为sayName的function对象.
咦?!,在上述的代码中,新对象myDog是如何指向了Dog的prototype的sayName函数(有点绕)呢?!
还记得"new 构造函数(参数)" 的过程中,都发生了什么么?!
1.创建一个新对象,对象的隐性引用"__proto__"指向构造函数的"prototype";
2.将构造函数的 "this" 指向新对象;
3.执行构造函数(初始化,向新对象添加属性);
4.返回这个对象.
下面对前面忽略的后半句进行解释.
每一个对象都有一个指向它的构造函数的prototype的隐性的引用(在构造的时候被赋值).
什么是隐性引用,就是你无法看到的,你无法使用myDog.prototype这样的方式获取prototype(在firefox和chrome中可以通过myDog.__proto__来访问).但是你却可以使用它的属性!所以你可以使用"myDog.sayName();"这种方式调用构造函数Dog的prototype的sayName函数.
如果对象的属性和对象的构造函数的prototype的属性重名了呢?!
自然是先访问对象本身的属性了.每当访问一个对象的属性时,先从对象自身查找,如果它自己没有,再查找它的__proto__指向的prototype对象有没有这个属性.
如果还没有呢?!
不会停止! 别忘了,prototype也是对象,它也有一个__proto__隐性引用,它会继续根据这个引用查找下去...查找下去...查找下去...查找下去,直到这个__proto__引用指向null为止.
在上面的例子中,很快就指向了null了.
var myDog = new Dog("Odie", "Yellow");
console.log(myDog.__proto__);
console.log(myDog.__proto__.__proto__);
console.log(myDog.__proto__.__proto__.__proto__);
在chrome中的结果:
Dog {say: function}//还有更详细的内容
Object {}
null
在firefox中的结果:
[object Object]
[object Object]
null
正是javascript这种链式查找的机制,使"继承"成为了可能(此处Dog继承Object).这个在后面讲.
javascript中的prototype实现了对象的共享机制,由同一个构造函数创造出来的对象,都有一个指向构造函数的prototype的隐性指针,再通过javascript访问属性的查找机制,就实现了共享.这类似于其他OO语言中的类的静态变量和静态函数.既然通过prototype共享function,自然也可以共享属性.
function Dog(name, color) { this.name = name; this.color = color; } Dog.prototype.sayName = function() { console.log(this.name); }; Dog.prototype.kind = "Dog"; var myDog = new Dog("Odie", "Yellow"); var youDog = new Dog("Oalive", "Black"); //通过对象读取 console.log(myDog.kind);//Dog console.log(youDog.kind);//Dog //但不能通过对象修改prototype属性 myDog.kind = "cat"; console.log(myDog.kind);//cat console.log(youDog.kind);//dog
上面这段代码,定义了一个prototype的kind属性,可以看到,可以直接通过对象访问该属性,却不可以通过对象进行修改.
为什么呢?!
前面讲过,每个对象都有一个指向prototype的隐性引用,这个引用是你摸不到的,"__proto__"这种写法只是chrome和firefox的浏览器支持,它并不是javascript标准(退一步说,即使是标准,你修改的方式也该是myDog1.__proto__.kind = "dog").你之所以能共通过对象访问到全局属性,是由于javascript查找机制决定的,在你访问一个属性时,在自身找不到,会继续向对象隐性引用指向的prototype继续找.
在最初讲对象的时候,特意强调了一下,访问一个对象的属性时,如果没有,并不会给对象追加一个属性,当时没有说后半句,后半句就是它会继续向它所指向prototype对象查找.这是javascript对访问属性时的处理方式.
而赋值操作呢,不会有这个查找的过程,如果没有,直接追加一个属性!所以myDog1.kind = "dog";这是给myDog1追加了一个kind属性,而对myDog1访问kind属性时,在自身就找到了这个属性,自然就不会继续查找了.
谁有prototype"正常"的引用呢——function——也就是构造函数嘛.
Dog.prototype.kind = "dog";
也可以是这样,但__proto__不是标准,即使是,也不建议这么写:
myDog1.__proto__.kind = "dog";
A.B.C三种创建对象方式的不同之处
exampleA:
//字面量方式创建对象,简单方便,可以认为,这是exampleB方式的简写
var myDog = {name: "Odie", color: "Yellow"};
exampleB:
//使用new操作符创建对象,再追加属性
var myDog = new Object();
myDog.name = "Odie";
myDog.color = "Yellow";
exampleC:
//一个很少使用的方式
var myDog = Object.create(new Object());
myDog.name = "Odie";
myDog.color = "Yellow";
A相当于B的简写方式,唯一的区别就是,通常,在浏览器中,A这种方式创建对象是不调用Object的构造函数的.
C呢,之前没有解释,这个方式跟A.B两种区别很大.
首先要说明Object.create(prototype,descriptors)这个函数,它可以创建一个指定原型的对象,参数prototype就是想要给创建出来的对象添加的指定原型//忽略后面的参数(它是可选的).
"var myDog = Object.create(new Object());"就是创建了一个以Object的实例为原型的对象,就是创建出来的对象的隐性指针指向了Object的实例.
过程类似于:
function(proto){
fucntion F(){};
F.prototype = proto;
return new F();
}
输出一下myDog的__proto__;
console.log(myDog.__proto__);//Object {}
console.log(myDog.__proto__.__proto__);//Object {}
console.log(myDog.__proto__.__proto__.__proto__);//null
为什么前两个都是"Object{}"呢,因为这是以Object的实例做为prototype的,myDog的__proto__指向Object的实例,而实例才真正指向Object的prototype.
如果这么写:
var myDog = Object.create(Object.prototype);
就和
var myDog = new Object();
是一样的了.
原型链图
CF --> constructor function -->构造函数
CFp -->constructor function prototype -->构造函数的原型
cf1--cf5 --> CF构造的实例
虚线 --> 隐性引用
实线 --> 正常引用
cf1--cf5这些对象共享CF的prototype;
CFp本身也是对象也有一个虚线的隐性引用;
CF也是对象,也有一个虚线的隐性引用,至于是什么,这要看浏览器是怎么实现的了.
prototype也有一个指向它的构造函数的引用
上面的图没有标明,其实prototype也有一个指向构造函数的引用,叫做constructor.这么说不太准确,应该说每个prototype也有一个指向相应的function的引用(prototype并不是构造函数独有的).
因此,是可以通过对象(经过prototype)访问constructor的.
var myDog = new Dog("Odie", "Yellow");
console.log(myDog.constructor);
怎么证明constructor是prototype的属性而不是对象myDog1的呢?!
Object有一个hasOwnProperty(name)函数来判断:
console.log(myDog.hasOwnProperty("constructor"));//false
console.log(myDog.hasOwnProperty("hasOwnProperty"));//false,hasOwnProperty是Object的prototype的函数哦.
console.log(myDog.hasOwnProperty("sayName"));//false,这是Dog的prototype的函数
console.log(myDog.hasOwnProperty("name"));//这个才是自己的属性
4继承
javascript的对象可以分为两部分,自己的属性和从prototype共享的属性.
那么,想要在javascript中实现继承,就需要:
1.子构造函数在执行过程中,也构造父构造函数构造出的属性;
2.继承父构造函数的共享属性——将子构造函数的prototype对象的隐性引用__proto__指向父构造函数的prototype,这样根据javascript的查找机制,就可以共享父类的prototype了;
javascript没有类,只有对象,非要向其他OO语言靠拢的话,可以认为把上述的父构造函数替换为父类.
构造父构造函数构造的属性
如果能在子构造函数构造的过程中,把它的this直接传递给父构造函数,在跑一遍父构造函数就好了.
javascript确实提供了这样一个方式.就是function对象call函数.
call函数的用法
还记得function的this引用吧——就是函数据以执行的对象的引用(可以叫做函数的运行环境引用,亦或者叫函数的上下文引用),使用call函数,就可以指定函数运行时this引用指向的对象.
call(thisObj,arg0,arg1....);
thisObj这个参数就是你要给函数运行时指定的对象.
arg0,arg1....是可选的,是函数运行时真正的参数.
用call函数继承父构造函数的属性:
//父构造函数
function SuperType(name) {
this.name = name;
}
//子构造函数
function SubType(name, age) {
SuperType.call(this, name);//调用父构造函数
this.age = age;
}
var sub = new SubType("js", 20);
console.log(sub.name);//js
console.log(sub.age);//20
console.log(sub.hasOwnProperty("name"));//true
console.log(sub.hasOwnProperty("age"));//true
前半部分就这么实现了,so easy是吧.
继承父构造函数的共享属性
还记得那个create函数么——创建一个指定原型的对象——可以指定被创建出来的对象的隐性引用(__proto__)指向那个对象.
那创建一个隐性引用指向父构造函数的prototype的对象:
var obj = Object.create(SuperType.prototype);
这是什么?!
这不就是我们想要的子构造函数的prototype对象么!!
//父构造函数 function SuperType(name) { this.name = name; } SuperType.prototype.sayName = function() { console.log(this.name); } //子构造函数 function SubType(name, age) { SuperType.call(this, name);//调用父构造函数 this.age = age; } SubType.prototype = Object.create(SuperType.prototype); SubType.prototype.sayAge = function() { console.log(this.age); } var sub = new SubType("js", 20); console.log(sub.name);//js console.log(sub.age);//20 sub.sayName();//js sub.sayAge();//20 console.log(sub.constructor);//function SuperType{...}
啊呀,sub的prototypetype怎么指向SuperType了?!
从头找,sub本身没有constructor这个属性,向他的原型找;
它的原型被修改了——"Object.create(SuperType.prototype)",这个对象也没有constructor属相啊,继续向它的原型找;
那就是SuperType.prototype了,它的constructor指向了SuperType.
SubType.prototype = Object.create(SuperType.prototype);
SubType.prototype.constructor = SubType;
console.log(sub.constructor);//function subType{...}
只要给SuperType.prototype加一个constructor属性就可以了.
继承就这样完成了~