JavaScript面向对象总结(长篇慢慢看)

来源:互联网 发布:sql怎么设置主键 编辑:程序博客网 时间:2024/05/05 14:15

JavaScript面向对象总结

一直没搞懂JavaScript的面向对象,它并没有Class这个概念却使用function创造出了一种基于原型的面向对象方法。研究了几天,最后总算得出一些结论:JavaScript面向对象的实质就是分割作用域。

个人觉得,计算机是没有面向对象这个概念的,因此必须采用某些机制在内存层面上将不同的数据进行分类和访问控制来实现类的概念。JavaScript这门语言,是编程语言从面向过程发展到面向对象的一个缩影。能深入的学习JavaScript是一件很爽的事情。

值类型与引用类型的区别

说之前先说明一下这两种类型,方便后边的理解

例1

var i=1;(function(i){    i=2;    alert(i);//弹出2})(i);alert(i);//弹出1

值类型:一开始外面的i与里面的形参i虽然值是一样的,但是内存地址并不一样,我们假设外面的i地址为”0X001”,里面的i地址为”0X002”。函数内部的i在执行x=2时,0X002被重新赋值为2,但是0X001位置的值不变,因此函数内部弹出2,函数外部弹出1。

例2

var obj={age:1};(function(obj){    obj.age=2;    alert(obj.age);//弹出2})(obj);alert(obj.age);//弹出2

值类型与引用类型

引用类型:外面的obj与里面的形参obj的值也是一样的,但是内存地址并不一样。但是注意:这里面存储的值是放置有{age:1}这个代码块的地址。跟刚才一样我们假设外面的obj地址为”0X001”,里面的obj地址为”0X002”,存储{age:1}的地址为0X999。这段代码是怎么运行的呢?首先,将0X999这个值写在0X001这个位置,传参时,将0X001处的值0X999写在0X002这个位置,所以0X001与0X002都保存了0X999这个值。然后在执行obj.age=2这句话时,先找到0X002,发现是引用类型,再找到0X999并修改。函数外边调用的时候,先找到0X001,发现是引用类型,再找到0X999并使用。因此这里无论怎样使用的都是一个值。

明白了这个,我们再玩一个好玩的东西。

例3

var obj={age:1};(function(obj){    obj={age:2};    alert(obj.age);//弹出2})(obj);alert(obj.age);//弹出1

值类型与引用类型

这个又是为什么呢?在执行obj={age:2}这句话时,相当于重新申请了一个新的内存空间如0X888存储{age:2},并将0X888写在函数里面obj的地址0X002那个位置,因此外面的obj与里面的obj完全没有交集。这里即使你使用{age:1}也和外边的{age:1}绝对不一样,只是都是1看不出差别。

开始找对象

什么是面向对象

在软件系统中,对象具有唯一的标识符,对象包括属性(Properties)和方法(Methods),属性就是需要记忆的信息,方法就是对象能够提供的服务。在面向对象(Object Oriented)的软件中,对象(Object)是某一个类(Class)的实例(Instance)。 —— 维基百科

总结一下,原来我们编程的方式是定义全局变量和函数,这些东西全部挂载到window上,window相当于一个柜子,我们把柴米油盐、衣服、零食等等所有东西乱七八糟全都放进去。这样会导致几个问题,一个是不好找东西,还有就是袜子和吃的放在一起,你懂得。对于程序来说,产生的问题就是,变量命名会冲突,还有就是当一个变量被几个程序使用的时候,几个程序都会修改该变量,必然会导致问题。

面向对象的思路相当于将不同类的东西分开,属于该类的就放在该类下面,是你的就只让你用,当然你也可以借用别人的东西来用。我们来具体介绍面向对象是如何实现这个套路的。

工场函数与构造函数

例1

function factoryFn(){    var obj = {};    obj.a=1;    obj.b=2    obj1=obj;    obj2=obj;    alert(obj1===obj2);//弹出true}factoryFn();

这个是毋庸置疑的,obj存储了一个对象的地址,并将它给obj1与obj2所以obj1===obj2。

例2

function factoryFn(){    var obj = {};    obj.a=1;    obj.b=2    return obj;}factoryFn();var obj1 = factoryFn();var obj2 = factoryFn();alert(obj1===obj2);//弹出false

这样就完全不一样的。这个函数就是所谓的工厂函数。第一次执行的时候创建一个函数内部创建一个obj对象,并将它给了obj1。第二次执行在其他地方又创建了一个obj给了obj2.所以obj1与obj2分开。由此可见,工厂函数达到了一个效果:把东西分类(模具),每类东西有自己的属性与方法,并且相互独立。而且可以根据这个类创造出实际的东西(产品)。产品与产品之间也相互独立。

例3

function ConstructorFn(){    this.a=1;    this.b=2;}var obj1= new ConstructorFn();var obj2= new ConstructorFn();alert(obj1===obj2);//弹出false

这个就是构造函数。《JavaScript高级程序设计》中说构造函数经历了4个步骤:

1、创建一个新对象

2、将构造函数的作用域赋给新对象(因此this就指向了这个新对象)

3、执行构造函数中的代码(为这个新对象添加属性)

4、返回新对象

书中说的很精准也很详细,就没必要再解释了。总之一句话就是:构造函数与工厂函数的实现原理是差不多的,而且效果也是一样的。但是构造函数做的更多,我们后边慢慢说。

构造函数与其他函数的唯一区别,就在于调用它们的方式不同。不过构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过new操作符来调用,那么它就可以作为构造函数;而任何函数,如果不通过new操作符来调用,那它跟普通函数也不会有什么两样。

构造器constructor

顾名思义,谁创建的这个对象,构造器就是谁。

function ConstructorFn(){    this.a=1;    this.b=2;}var obj1= new ConstructorFn();console.log('obj1的构造器:'+obj1.constructor);var obj2 = {};console.log('obj2的构造器:'+obj2.constructor);console.log('Math的构造器:'+Math.constructor);console.log('Date的构造器:'+Date.constructor);console.log('Array的构造器:'+Array.constructor);console.log('String的构造器:'+Array.constructor);console.log('Object的构造器:'+Object.constructor);console.log('Function的构造器:'+Function.constructor);

构造器执行结果

原型prototype基本概念

说原型之前先来看一个例子:

function fn(){    var a=1;}fn.name='fn';fn.age=1;console.dir(fn);

函数是对象

这个例子说明,函数也是一个对象。我们可以在上边挂载属性和方法。这个知识点后边会用到。

学过JavaScript的都知道,JavaScript是一门基于原型的面向对象语言。研究了几天JavaScript面向对象总算知道了原型的重要性。虽然不知道原型在底层是如何实现的,对于Java这种很严格的面向对象语言也没有深入研究过,但是并不是很影响我研究JavaScript的原型。下面说说我的理解。

在使用构造函数的时候,每创建一个新对象,相当于为该对象开辟一个新空间,对象与对象之间相互独立,且都拥有相同的方法和属性(对象与对象之间相当于复制而不是引用)。这样就造成一个问题,内存浪费。如果有1000个对象,就要开辟1000个空间。

因此,JavaScript引入了原型这个概念。

原型对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor属性,这个属性是一个指向prototype属性所在函数的指针。

原型与构造器

修改原型指向constructor属性使用

一般给构造函数添加原型的方式

function Fn(){}Fn.prototype.name=1;Fn.prototype.age=2;console.log('Fn原型的构造函数是:'+Fn.prototype.constructor);

Fn原型的构造函数

换一种方式

function Fn(){}Fn.prototype={    name:1,    age:2};console.log('Fn原型的构造函数是:'+Fn.prototype.constructor);

Fn原型的构造函数

原型那个章节说过原型的来历,所以这里也好理解。
{
name:1,
age:2
}
这个东西是一个我们手动创造出来的对象,因此它的构造器是Object。所以我们需要修改constructor属性指向构造函数。

完整的修改构造器程序

function Fn(){}Fn.prototype={    constructor:Fn,    name:1,    age:2};console.log('Fn原型的构造函数是:'+Fn.prototype.constructor);

Fn原型的构造函数

原型链

原型链是JavaScript实现面向对象的重要部分。原型链指明了对象查找方法和属性的规范,类似于三层嵌套函数中同名变量作用域的问题。
废话少说,直接来代码

Object.prototype.name='Object';function Fn(){    this.name='f1';}Fn.prototype.name='Fn';var f1 = new Fn();alert(f1.name);

当我们按顺序依次注释掉
this.name=’f1’;
Fn.prototype.name=’Fn’;
时,程序会分别弹出f1、Fn、Object。
原型链

prototype与proto

前面的原型链中我们已经介绍过,当某个对象上没有某个成员的时候,我们可以通过原型链查找其构造函数上是否有这个成员。然而有一个需要特别注意的问题是,通过这种方法,我们只能使用其原型链上的成员,却不能设置。

例1

function Fn(){};Fn.prototype.age=1;var f1 = new Fn();alert(f1.age);//弹出1f1.age=2;alert(f1.age);//弹出2alert(Fn.prototype.age);//弹出1

如果想要通过对象设置其构造函数上的prototype,我们需要借助对象上的proto属性。

例2

function Fn(){};Fn.prototype.age=1;var f1 = new Fn();alert(f1.age);//弹出1f1.__proto__.age=2;alert(f1.age);//弹出2alert(Fn.prototype.age);//弹出2

那么prototype与proto有什么区别呢?

function Fn(){};Fn.prototype.age=1;var f1 = new Fn();console.log(Function.prototype);console.log(Fn.__proto__);console.log(Fn.prototype);console.log(f1.__proto__);console.log(f1.prototype);console.log(f1.__proto__===Fn.prototype);

prototype与__proto__

我们一点一点看上面例子的结果:

Fn是Function创造出来的一个对象,f1是Fn创造出来的一个对象。
最后一句证明:一般情况下,对象的proto与其构造函数的prototype是等价的。
因此Function作为Fn的构造函数Function.prototype===Fn.proto
Fn作为f1的构造函数Fn.prototype===f1.proto
由于JavaScript只会默认为函数创建prototype属性,因此f1的prototype属性为undefined。

面向对象

推荐一个博客:

http://www.cnblogs.com/Leo_wl/p/5734794.html

面向对象的三个基本特征是:封装、继承、多态、抽象。

但是对于JavaScript来说貌似并不是很关注抽象与接口。
面向对象

图片来源
:http://www.360doc.com/content/12/0102/15/306774_176667425.shtml

封装

封装,也就是把客观事物封装成抽象的类。这个类拥有自己独特的属性与方法,同时它也可以根据需要决定这些成员能否被外部使用,能否被其他类使用,能否被其对象使用。从而衍生出下面几个概念:公有成员(属性和方法)、私有成员、特权方法。

公有成员

这个是最容易理解的概念,也是用的很多的概念。

function Father(){    this.name='父亲';//公有属性    this.fn = function(){//公有方法        alert(1);    }}Father.prototype.fnPro(){//通过原型实现的公有方法    alert(2);}

通过原型实现的均为公有成员,他们被所有由该类创造的对象所共享。
公有成员可以被所有由该类创造的对象使用,而且可以被其他类继承。

私有成员

function Father(){    var name = '私有';//私有属性    function fnPri(){//私有方法        alert(1);    }}

说白了,其实这个就是一个局部变量。由该类创建出来的对象不能直接访问该私有成员,但是可以通过定义特权方法访问。

特权方法

由于私有成员无法被对象和其他类访问,但有时候我们确实需要使用它们,因此有了特权方法。

特权方法,本身也是公有的方法,任何生成的实例都可以调用。但这里之所以称为“特权”方法,是因为该方法由于放到了构造函数的内部,使得其具有权限访问构造函数执行过程的作用域链的各个属性和函数,包括私有属性和私有方法,但同时由于作为实例的一个成员方法,每new一个对象,都会创建一个单独的特权方法的拷贝。
从根本目的上看,特权方法的存在是为了访问私有方法和私有属性并且发布为实例的公共成员方法。这里其实已经形成了一个闭包。
摘自:http://blog.csdn.net/tenfyguo/article/details/5780986

function Father(){var name = '私有';//私有属性function fnPri(){//私有方法    alert(1);}this.fnPub = function(){    alert(name);}}var f1 = new Father();f1.fnPub();//弹出私有

工具方法

工具方法本身跟构造函数的作用并没有太大关系。构造函数是一个函数,但它本身也是一个对象,因此可以拥有属性与方法,自然可以在上面扩展工具方法。jQuery中有很多工具方法,如$.each()

具体介绍参看这位大神的文章

http://www.cnblogs.com/kissdodog/archive/2012/12/27/2835561.html

function Father(){  }Father.name = '静态';Father.fnSta = function(){    alert(1);}var f1 = new Father();alert(f1.name);//undefinedf1.fnSta();//not a function

工具方法是该构造函数所独有,不能被其创造出来的对象所使用。工具方法常用于定义常量、保持唯一值的存储或者定义特殊方法,比如Math.PI等。

继承

继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。JavaScript继承中包括类式继承、构造函数式继承、组合式继承、原型式继承、寄生式继承、寄生组合式继承、多继承等。

类式继承

将父类的一个实例挂载在子类构造函数的原型上,并修改子类构造函数原型的构造器为子类构造函数。
比较不好懂,还是上代码上图吧。

例1

function Father() {
this.name = ‘父亲’;
}
Father.prototype.age = 1;
function Child() {
}
Child.prototype = new Father(); //子类的原型是父亲的一个实例
console.log(Child.prototype.constructor);
Child.prototype.constructor = Child; //更改子类的构造器属性
console.log(Child.prototype.constructor);
var c1 = new Child();
console.log(c1.name);
console.log(c1.age);

类式继承
类式继承

构造函数式继承

在子构造函数中调用父构造函数,并修改父构造函数中的this指向。

function Father() {    this.name = '父亲';}function Child() {    Father.call(this);//将Father函数内部的this指向强制转为Child}var c1 = new Child();console.log(c1.name);

Father()在执行的时候函数内部的this指向为window。现在强制让this指向调用子类构造函数的对象。当子类构造函数被c1变量new的时候,this就变成了c1。
这种方法无法继承父类原型中的方法和属性。

组合继承

由于构造函数式继承不能继承原型上的属性与方法,因此有了组合继承。
组合继承就是普通属性和方法使用构造函数式继承,而原型上的属性与方法采用类式继承。

function Father() {    alert(this);    this.name = '父亲';}Father.prototype.age=1;function Child() {    Father.call(this);}Child.prototype = new Father();Child.prototype.constructor = Child;

原型式继承

function object(o) {
function fn() {};
fn.prototype = o;
return new fn();
}
function Father() {
this.name = ‘父亲’;
}
Father.prototype.nums = [1, 2];
var child = object(new Father());
console.log(child.name);//父亲
console.log(child.nums);//[1,2]

个人觉得这种继承方式跟类式继承很像。类式继承是子类构造函数的原型,这里变成了object里的那个构造函数的原型。

寄生式继承

function object(o) {
var clone = Object(o);
return clone;
}
function Father() {
this.name = ‘父亲’;
}
Father.prototype.nums = [1, 2];
var child = object(new Father());
console.log(child.name);//父亲
console.log(child.nums);//[1,2]

这种继承方式跟类式继承也很像,只不过这里借用了一个clone的Object对象。

寄生组合式继承

将寄生式继承与构造函数式继承结合起来。

function object(subType,superType){    var prototype = Object(superType.prototype);    prototype.constructor = subType;    subType.prototype = prototype;}function Father() {    this.name = '父亲';}Father.prototype.nums=[1,2];function Child(){    Father.call(this);}object(Child,Father);var c1 = new Child();

多继承

通过遍历父类原型上的每个成员像子类上添加对应成员。

function object(subType,superType){    for(var attr in superType.prototype){        subType.prototype[attr] = superType.prototype[attr];    }}function Father() {    this.name = '父亲';}Father.prototype.nums=[1,2];function Child(){    Father.call(this);}object(Child,Father);var c1 = new Child();console.log(c1);c1.nums.push(4);console.log(Father.prototype.nums);

类式继承、原型式继承、寄生式继承区别

这里就简单地说一下吧,因为如果仔细研究起来确实太复杂,也没有太大必要。几种继承方式的区别就是继承过来的对象挂载位置的不同,从而在原型链上产生了层级关系,根据原型链查找规则从而可以找到对应的成员。

基于prototype继承方式的共性问题

由于原型上的属性共用以及引用类型在实际使用时为地址的特点,继承往往会出现这种问题:
父类原型上挂载了一个引用类型,子类继承父类后,子类修改自身上这个引用类型内部的某个值的时候,会同时修改父类上对应的值

例1

function object(subType, superType) {
for(var attr in superType.prototype) {
subType.prototype[attr] = superType.prototype[attr];
}
}
function Father() {
}
Father.prototype.obj = {
name: ‘父亲’
};
function Child() {
Father.call(this);
}
object(Child, Father);
var c1 = new Child();
c1.obj.name = ‘孩子’;//!!!这里与例2不同!!!
console.log(c1.obj.name); //输出孩子
console.log(Father.prototype.obj.name); //输出孩子

请仔细区分例1与例2的区别。

例2

function object(subType, superType) {
for(var attr in superType.prototype) {
subType.prototype[attr] = superType.prototype[attr];
}
}
function Father() {
}
Father.prototype.obj = {
name: ‘父亲’
};
function Child() {
Father.call(this);
}
object(Child, Father);
var c1 = new Child();
c1.obj = {//!!!这里与例1不同!!!
name: ‘孩子’
};
console.log(c1.obj.name); //输出孩子
console.log(Father.prototype.obj.name); //输出父亲

造成两个例子不同的原因在prototype与proto那节已经阐述过,这里就不在赘述。总之在继承引用类型的时候要时刻保持警惕,以免不小心修改父类的属性。

多态

多态是指子类在继承父类之后,还可以在父类的基础上拥有自己的方法和属性,也可以改写从父类继承的方法和属性。

function Father() {
}
Father.prototype.fn = function(){
alert(1);
};
function Child() {
}
Child.prototype = new Father(); //子类的原型是父亲的一个实例
Child.prototype.constructor = Child; //更改子类的构造器属性
Child.prototype.fn = function(){
alert(2);
}
Child.prototype.fn1 = function(){
alert(3);
}
var f1 = new Father();
var c1 = new Child();
f1.fn();
c1.fn();
c1.fn1();

0 0
原创粉丝点击