面向对象谈对象
一切事物皆对象
对象具有封装和继承特性
对象与对象之间使用消息通信,各自存在信息隐藏
对象(object),台湾译作物件,是面向对象(Object Oriented)中的术语,既表示客观世界问题空间(Namespace)中的某个具体的事物,又表示软件系统解空间中的基本元素。
在软件系统中,对象具有唯一的标识符,对象包括属性(Properties)和方法(Methods),属性就是需要记忆的信息,方法就是对象能够提供的服务。在面向对象(Object Oriented)的软件中,对象(Object)是某一个类(Class)的实例(Instance)。 —— 维基百科
Javascript是一种基于对象(object-based)的语言,你遇到的所有东西几乎都是对象。但是,它又不是一种真正的面向对象编程(OOP)语言,因为它的语法中没有class(类)。
那么,如果我们要把"属性"(property)和"方法"(method),封装成一个对象,甚至要从原型对象生成一个实例对象,我们应该怎么做呢?
构造函数模式
1 function Cat(name, color) { 2 this.name = name; 3 this.color = color; 4 this.eat = function () { 5 return '吃老鼠...'; 6 }; 7 };
构造函数的一些基本属性
1.构造函数没有new Object,但它后台会自动var obj = new Object
2.this就相当于obj
3.构造函数不需要返回对象引用,它是后台自动返回的
1.构造函数也是函数,但函数名第一个字母大写
2.必须new 构造函数名(),new Box(),而这个Box第一个字母也是大写的
3.必须使用new 运算符
这样的好处是构造函数的实例对象自动含有一个constructor属性,指向它们的构造函数
1 var cat = new Cat("Tom","block"); 2 console.info(cat1.constructor == Cat); //true
构造函数和普通函数的唯一区别,就是他们调用的方式不同。只不过,构造函数也是函数,必须用new运算符来调用,否则就是普通函数。
构造函数方法很好用,但是存在一个浪费内存的问题,如果在Cat下面加了type属性和eat等公共的方法,每次创建对象是不是要浪费很多的空间呢,
因为构造函数内部的 this.eat = function () 都会创建了一个地址的引用,也就是内部对象不一样了。可不可以让eat这个属性对象只占用一个内存地址呢。
我们可以通过构造函数外面绑定同一个函数的方法来保证引用地址的一致性
1 functon Cat(name,color){ 2 this.name=name; 3 this.color=color; 4 this.eat = eat; 5 } 6 function eat(){ 7 return "吃老鼠"; 8 }
虽然使用了全局的函数run()来解决了保证引用地址一致的问题,但这种方式又带来了一个新的问题,全局中的this在对象调用的时候是Box本身,而当作普通函数调用的时候,
this又代表window,显然这种封装不尽人意!
Prototype:原型模式
Javascript规定,每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。
这意味着,我们可以把那些不变的属性和方法,直接定义在prototype对象上。我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个对象,
它的用途是包含可以由特定类型的所有实例共享的属性和方法
逻辑上可以这么理解:prototype通过调用构造函数而创建的那个对象的原型对象。使用原型的好处可以让所有对象实例共享它所
包含的属性和方法。也就是说,不必在构造函数中定义对象信息,而是可以直接将这些信息添加到原型中。
1 function Box(){} 2 Box.prototype.name = "Jim"; 3 Box.prototype.age = 100; 4 Box.prototype.run = function(){alert("运行中")}; 5 var box1 = new Box(); 6 var box2 = new Box(); 7 alert(box1.run == box2.run); //true
为了更进一步了解构造函数的声明方式和原型模式的声明方式,我们通过图示来了解一下
原型模式方式如下
在原型模式声明中,多了两个属性,这两个属性都是创建对象时自动生成的。__proto__属性是实例指向原型对象的一个指针,
它的作用就是指向构造函数的原型属性constructor。通过这两个属性,就可以访问到原型里的属性和方法了。那么原来Cat对象的eat公共
方法可以用原型来直接创建。
PS:IE浏览器在脚本访问__proto__会不能识别,火狐和谷歌浏览器及其他某些浏览器均能识别。虽然可以输出,但无法获取内部信息。
判断一个对象是否指向了该构造函数的原型对象,可以使用isPrototypeOf()方法来测试。
1 console.info(Box.prototype.isPrototypeOf(box1));//true
原型模式的执行流程:
1.先查找构造函数实例里的属性或方法,如果有,立刻返回;
2.如果构造函数实例里没有,则去它的原型对象里找,如果有,就返回;
虽然我们可以通过对象实例访问保存在原型中的值,但却不能访问通过对象实例重写原型中的值。
1 var box1 = new Box(); 2 var box2 = new Box(); 3 console.info(box1.name);// "Jim" 4 box1.name="Jack"; 5 console.info(box1.name);// "Jack" 就近原则 6 console.info(box2.name);// "Jim" 没有被修改!! 实例属性不会共享,所以box2访问不到实例属性,就只能访问原型
如果想要box1也能在后面继续访问到原型里的值,可以把构造函数里的属性删除即可,具体如下:
1 var box1 = new Box(); 2 console.info(box1.name);// "Jim" 3 box1.name="Jack"; 4 console.info(box1.name);// "Jack" 5 delete box1.name; 6 console.info(box1.name);// "Jim"
如何判断属性是在构造函数的实例里,还是在原型里?可以使用hasOwnProperty()函数来验证:
1 console.info(box1.hasOwnProperty('name'));// false 说明当前在构造函数里 {实例里有返回true,否则返回false}
构造函数实例属性和原型属性示意图
in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。 console.info("name" in box1);// true
1 //判断原型中是否存在属性 2 function isProperty(object, property) { 3 return !object.hasOwnProperty(property) && (property in object) 4 } 5 var box = new Box(); 6 console.info(isProperty(box,'name')); //true 7 console.info(isProperty(box,'pig')); //false
另外: 为了让属性和方法更好的体现封装的效果,并且减少不必要的输入,原型的创建可以使用字面量的方式:
1 function Box(){}; 2 Box.prototype = { 3 name : "Jim", 4 age : 100, 5 run : function(){ 6 return this.age+" "+this.name; 7 } 8 };
使用构造函数创建原型对象和使用字面量创建对象在使用上基本相同,但还是有一些区别,
字面量创建的方式使用constructor属性不会指向实例,而会指向Object,构造函数创建的方式则相反。
1 var box = new Box(); 2 console.info(box instanceof Box); //true 3 console.info(box instanceof Object); //true 4 console.info(box.constructor == Box); //false 5 console.info(box.constructor == Object); //true
如果想让字面量方式的constructor指向实例对象,那么可以这么做
1 Box.prototype={ 2 constructor:Box, //直接强制指向即可 3 };
PS:字面量方式为什么constructor会指向Object?因为Box.prototype={};这种写法其实就是创建了一个新对象。而每创建一个函数,就会同时创建它prototype,这个对象也会自
动获取constructor属性。所以,新对象的constructor重写了Box原来的constructor,因此会指向新对象,那个新对象没有指定构造函数,那么就默认为Object。
原型的声明是有先后顺序的,所以,重写的原型会切断之前的原型.
1 function Box(){}; 2 Box.prototype = { 3 constructor:Box, 4 name : "Jim", 5 age : 100, 6 run : function(){ 7 return this.age+" "+this.name; 8 } 9 }; 10 11 12 Box.prototype={ 13 age : 28 14 }; 15 var box = new Box(); 16 console.info(box.run()); //box.run is not a function
PS:原型对象不仅仅可以在自定义对象的情况下使用,而ECMAScript内置的引用类型都可以使用这种方式,并且内置的引用类型本身也使用了原型。
1 //查看sort是否是Array原型对象里的方法 2 alert(Array.prototype.sort); 3 alert(String.prototype.substring); 4 5 //内置引用类型的功能扩展 6 String.prototype.addstring = function () { 7 return this + ',被添加了!'; 8 }; 9 10 var box = 'Lee'; 11 alert(box.addstring());
尽管给原生的内置引用类型添加方法使用起来特别方便,但我们不推荐使用这种方法。因为它可能会导致命名冲突,不利于代码维护。
原型模式创建对象也有自己的缺点,它省略了构造函数传参初始化这一过程,带来的缺点就是初始化的值都是一致的。而原型最大的缺点就是它最大的优点,那就是共享。
原型中所有属性是被很多实例共享的,共享对于函数非常合适,对于包含基本值的属性也还可以。但如果属性包含引用类型,就存在一定的问题
1 function Box(){}; 2 Box.prototype = { 3 constructor:Box, 4 name : "Jim", 5 age : 100, 6 family:['父亲', '母亲', '妹妹'], 7 run : function(){ 8 return this.age+" "+this.name+" "+this.family; 9 } 10 }; 11 var box1 = new Box(); 12 box1.family.push("哥哥"); 13 var box2 = new Box(); 14 console.info(box1.run()); // 100 Jim 父亲,母亲,妹妹,哥哥 15 console.info(box2.run()); // 100 Jim 父亲,母亲,妹妹,哥哥
PS:数据共享的缘故,导致很多开发者放弃使用原型,因为每次实例化出的数据需要保留自己的特性,而不能共享。