title: 面向对象一、原型
date: 2017-06-16 17:01:01
tags: javascript笔记
面向对象的概念
面向对象是一种编程思想,核心是解决任何问题的时候首先试图去找到一个对象来帮助解决问题
在编程中,面向对象是调度者,从根本上是将面向过程封装,所以面向过程不可弃之不用。面向过程是执行者,执行顺序一般情况不能打乱。
面向对象的优点:
-
代码的灵活度高,代码执行顺序可以打乱。面向过程的代码不能打乱。
-
可维护性高,出现bug只需要在对象上去调试。
-
扩展性高,扩展时只需要维护局部模块。
面向对象的缺点:
-
可能会造成代码复杂度提高
-
代码可读性相对不好
js语言的特点
-
弱类型
-
多范式
-
基于对象的语言:
-
在Javascript中,一切的根源都是对象,并没有面向对象的一些概念,所以说是基于对象的语言,通常把构造函数当做一个模板,通过模板对建立对象
-
在其他面向对象的语言中,有类这个概念,Javascript用构造函数来模拟类,类和构造函数都是起到了模板的作用
-
不是面向对象的语言,只是用面对象向这种思想来模拟
- 基于原型的语言:
- 弱类型的语言基本都有原型存在,在面向对象的语言中,是类和类之间继承,而Javascript中只能让对象和对象之间继承,
原型的介绍
原型的概念
就是一个函数的prototype属性所引用的对象,原型是Javascript内置的,只要声明了一个函数,那么原型就自动存在了。
function fn (){} // 这是一个构造函数
console.log(fn.prototype) // 这个构造函数的原型
console.log(fn.prototype.constructor) //这个构造函数
// 所以构造函数和它的原型是能互相访问的。
原型的意义
通过同一个构造函数创建出的所有对象都共享这个构造函数的原型,也就是说上述创建出的所有对象,可以直接访问到原型上的任何成员(属性和方法)。
function fn (){}
var f = new fn
f.constructor //就是fn这个构造函数,所以在fn这个构造函数里增加属性和方法对象f是可以直接访问的
原型的本质
就是一个对象。在原型中创建方法就和给对象加方法一样。
原型的好处
可以实现数据共享。用下面的代码列举问题来看一下原型的好处
function Preson () {
this.talk = function(){ console.log("talk") }
}
var jim = new Preson;
jim.talk();
var john = new Preson;
john.talk();
// 问题:以上创建的两个对象访问了两次构造函数的talk()方法,这样对象每次访都是创建一个新的只属于这个对象的函数,每创建一个方法都是占用一块内存,而方法中的逻辑实际上都是一样的。这就相当于浪费了一块内存的位置
// 解决方法:把建立在构造函数内的方法放在一个公共的地方,而这个公共的地方必须是该构造函数创建出来的,这样对象才能访问到,也就是该构造函数的原型上,实现数据共享
fn.prototype.talk = function(){ console.log("talk") }
获取原型的方式:
函数:函数名.prototype
对象:对象.__proto__
对象的组成部分:
对象本身和它的原型组成
每个对象都有__proto__
属性,也就是说每个对象都有原型,所以说Javascript是基于原型的语言,
对象的类型:
就是该对象的构造函数的名字,Javascript虽然是弱类型语言,并不是没有类型,而是不注重类型的存在,体现在所有对象用typeof去检测都是object,所以也可以说所有对象的原型都是Object.prototype。
自定义一个数组对象的原型还是一个数组对象。他们的构造函数就是Array内置函数,包括Object,Array,Date都是内置函数。new后边的都是函数。
原型的归属:
原型的属性:给原型一个归属,也就是什么什么的原型,通常说原型是站在函数的角度去认识原型,那么站在函数的角度来说,原型可以被称为该函数的原型属性。
原型的对象:是站在对象的角度来看原型,此时原型可称为是这个对象的原型对象。
这两者只是称谓不同,实际上都是同一个原型。
__proto__
的兼容性处理:
两个下划线的属性是有兼容性的,这不是W3C的标准属性,只是浏览器给提供的便利的东西。
function getPrototype(obj) {
// 判断浏览器是否兼容__proto__
if (obj.__proto__) { // 如果支持
return obj.__proto__;
} else { // 如果不支持
// 获取该对象的构造函数
// 在通过此函数的prototype属性获取其原型对象
return obj.constructor.prototype;
}
}
// 三元表达式写法
function getPrototype(obj) {
return !!obj.__proto__ ? obj.__proto__ : obj.constructor.prototype;
}
标准构造函数写法
主要就是要考虑哪些属性应该保留在构造函数内部,哪些属性提取出来放在原型上。
和对象息息相关的属性,这些属性都要写在构造函数内部。像姓名、年龄这些属性,是随着对象不同而改变的,所以没法放在原型上,而是要放到构造函数内部。
而那些为了共享的属性并且是每个对象都具有的属性,值也不会随对象变化而变化是确定的值。可以写在原型上。比如每个人都生活在地球上。
在一般情况下,方法被认为是所有对象所共有的。比如一般情况下人都会说话。所以所有方法都应该放在原型上。
原型的特性
动态性
给原型扩展成员会直接反应到已创建的对象身上。
function A() {}
A.prototype.color = 'black';
var a = new A;
var ad = new A;
// 已经创建对象之后再去扩展原型上的属性,也会反应到对象身上
A.prototype.makefood = function (){ console.log('做饭')}
a.makefood(); // 做饭
ad.makefood(); // 做饭
置换原型对象,不会反映到已创建出来的对象。但是会直接影响之后创建出来的对象。
function A() {}
A.prototype.color = 'black';
var a = new A;
A.prototype = {
constructor: A,
makeup: function() {
console.log('我会化妆.');
}
};
var na = new A;
console.log(a.color); // black,因为a是置换对象之前创建的,所以它的原型就是A置换之前的原型。
a.makeup(); // 报错,因为a是在置换原型之前创建的对象。
na.makeup(); // 我会化妆.因为na是在置换原型之后创建的对象
console.log(na.color); // undefined 因为这是在置换之前扩展的。扩展之后color属性就没了
唯一性
由同一函数创建出来的所有对象,共享同一个原型对象。
// 由同一个构造函数创建出的对象都共享同一个原型。
function A() {}
A.prototype.color = 'black';
var a = new A;
var ad = new A;
// 这两个对象的原型对象全等,也就是同一个,并且共享原型对象的属性。
console.log(a.__proto__ === ad.__proto__); // true
console.log(ad.color); // black
console.log(a.color); // black
不可变性
对象是无法改变原型对象上的任何成员
function A() {}
A.prototype.color = 'black';
var a = new A;
// 更改a的color属性只能改变自身不会改变其他成员,并不会改变它的原型上的color属性,所以也不会改变ad的color。
a.color = 'goldyellow';
var ad = new A;
console.log(ad.color); // black
console.log(A.prototype.color); // black
console.log(a.color); // goldyellow
继承性
所有对象都继承自它的原型对象
function A() {}
A.prototype.color = 'black';
var a = new A; // a和na都是构造函数A创建出的对象,,都继承了A的原型
var na = new A;
console.log(a.color); // black
console.log(na.color); // black
面向对象的三大特性
封装性
把复杂的实现过程包装并隐藏起来,然后提供一个接口来给用户使用。
封装的好处
-
实现代码的重复利用。
-
实际使用中,只要出现重复代码逻辑就要考虑封装成一个函数,如果该函数和一些变量关联性比较大,那么就可以将函数封装成一个对象。
-
私密性(安全性),封装后用户看不到复杂的内部代码,不会误操作覆盖封装的变量。
-
封装时尽量保持函数或对象功能的单一性,便于日后维护。
继承性
-
概念:就是指一个对象有权去访问另一个对象的属性和方法,自己没有的属性和方法可以去访问另一个对象去获得,在js中只要让一个对象去访问另一个对象的属性和方法的话就必须要建立继承,任何对象都继承自己的原型对象。
-
在js中继承是对象与对象之间,其他面向对象语言(c,java,objectC等)都是类与类之间的继承。类在其他语言里就相当于模板的意义,在js中模板是构造函数,那么通过同一个构造函数(模板)创建出来的对象都继承函数里的属性和方法(ES6之前的方式)
-
在实际开发中两种继承方式可以组合起来应用。
集成的实现方式1:基于原型
扩展原型:在原有的原型上进行相应的扩展,实现继承。
在对象的构造函数的原型上进行扩展,那么该对象也就继承了扩展的内容
function A () {}
var a = new A;
// a本身没有printA这个方法,但是它的模板创建了这个方法,所以a也继承了这个方法
A.prototype.printA = function () { console.log("扩展原型") }
a.printA();
置换原型:将要被继承的对象,直接替换掉原有的原型。
假如b要继承a,就把b构造函数的原型直接替换成new a(传参)
// 首先创建了这个构造函数用parent代表 child要继承自parent
function parent () {
this.name = 'tom';
}
// 给这个模板的原型创建了一个方法
parent.prototype.printC = function () {
console.log("c");
console.log(this.name);
}
// 又创建了一个构造函数。child代表继承自parent。
function child () {}
// 让child的原型 = 模板parent创建的对象。parent函数的name和printC也继承给了child,同时传参数也不影响parent函数
child.prototype = new parent();
//创建对象c,因为child的原型已经和函数parent一样,所以用c可以直接访问name和printC了
var c = new child;
c.printC(); // 打印c
console.log(c.name); // 打印tom
集成的实现方式2: 拷贝继承
拷贝继承:将别的对象上的所有成员拷贝一份添加到的当前对象本身,拷贝继承没有任何对原型的操作。
// 创建一个对象parent
var parent = {
print: function() {
console.log('i am parent');
},
name: 'parent'
}
// 创建一个对象child
var child = {
name: 'child'
}
// child没有print方法,那么可以拷贝一份过来
// 拷贝步骤:
// 1、遍历parent。
for (var k in parent) {
//这样就将parent的所有属性都拷贝了过来,k就是name和print,child依次更改和创建了这两个属性,并且将parent对应的属性值赋值给child.
child[k] = parent[k];
}
child.print(); // 打印i am parent
// 拷贝继承概念部分结束
下面是一个问题,就是代码重复的问题,当child还要继承parent1的时候就要再写一遍遍历,造成了代码重复
var parent1 = {
print1:function(){
console.log('print1');
}
}
for (var k in parent1) {
child[k] = parent1[k];
}
child.print1(); // 打印print1,但是如果要拷贝多个parent那么代码会重复
所以可以封装成一个函数,封装为child对象的一个方法extend,谁调用extend方法就是给谁实现继承
// 新建一个child对象
var child = {}
// 给child创建一个名为extend方法,里边的函数就是封装的拷贝方法
child.extend = function(parent) {
var k;
// parent是传的参数
for (k in parent) {
this[k] = parent[k]
}
}
// 第一步封装完成,但是问题是只能往里传一个对象
// 实现继承多个对象
child.extend = function() {
// arguments是传入参数的数据的数组,在这里也就是传入的对象数量
var args = arguments;
//遍历atguments上的所有对象
//依次将遍历的每个对象的成员添加到child
for (var i = 0, l = args.length; i < l; i++) {
//判断传入的是否为对象
if (typeof obj === 'object') {
for (var k in args[i]) {
this[k] = args[i][k]
}
}
}
}
// 调用这个对象的拷贝方法并且传一个参数,参数是对象
child.extend({
name: 'child',
print: function() {
console.log(this.name);
}
})
child.print(); // 打印child
集成的实现方式3:对象冒充
对象冒充:在一个构造函数中可以动态的添加一个parent方法指向,用已有的构造函数,然后调用parent方法去实例化当前对象的一部分成员(或全部成员),这种方式被称为对象冒充。
function parent (name,age,gender) {
this.name = name;
this.age = age;
this.gender = gender
}
function child(name,age,gender) {
this.parent = parent;
this.parent(name,age,gender);
delete this.parent;
// child通过这个属性冒充parent,通过这个构造函数创建对象也会有parent里的成员
// 注意:child利用完parent属性后记得删除
}
集成的实现方式4:借调函数
function parent (name,age,gender) {
// body...
this.name = name;
this.age = age;
this.gender = gender;
}
// 和冒充类似,这是用call方法实现的
function child (name,age,gender,address) {
// body...
parent.call(this,name,age,gender);
this.address = address;
}
var c= new child('tom',28,'男','yueqiu');
集成的实现方式5:Object.create(parent) (置换原型的原理)
// 方法的介绍 Object.create(parent); 返回一个对象并继承自传入参数parent
// 用基于原型:置换原型的方式来继承
var obj = {
name:'tom',
print:function(){
console.log(this.name);
}
}
// 将obj当做参数传进来,newObj就继承了obj
// 声明一个新变量来接收继承自parent的对象
var newObj = Object.create(obj)
newObj.print();
// 下面是Object.create解决兼容性问题
if(!Object.create){
Object.create = function(parent){
function F () {
F.prototype = parent;
return new F;
}
}
}
多态性
体现在继承中的概念。比如某对象A继承自某对象B,B对象的某个方法在A中并不适用。然后A对象重写该方法,那么这个就是多态性。
具体体现在子对象和父对象之间,在父对象中的同一方法在各个子对象中的实现行为不同。
原型链
原型链是从当前对象到Object.prototype
之间,存在一条层次分明,逐级递进的体现继承关系的链式结构
所有对象都有__proto__
属性
原生对象继承自Object.prototype
,具有constructor
属性;如果置换了原型,记得要添加constructor
属性
函数具有prototype
属性
var o = {}; //Object对象,对象都有__proto__属性
// 对象的__proto__是它的原型对象
console.log(o.__proto__)
// 对象的constructor是它的数据类型Object
console.log(o.constructor)
// 对象的原型就是Object.prototype
console.log(Object.prototype === o.__proto_);
console.log(o.__proto__.__proto__);
// 也就是console.log(Object.prototype.__proto__);
// 返回null
// 所以 o -> Object.prototype -> null
// Object对象的继承层次:
// obj -> Object.prototype -> null
// 以数组为例
var arr = [];
console.log(arr.constructor === Array); // true
console.log(arr.__proto__ === Array.prototype); // true
// arr -> Array.prototype -> Object.prototype -> null
用一张图来表示原型链:这是最简单最基础的原型链
image用两个例子来深入体会原型链:
function parent() {}
var p = new parent;
image
function A() {}
function B() {}
B.prototype = new A; // 这时B.prototype的构造函数就是A(),
var b = new B;
image
属性搜索原则
当访问对象成员时,首先在当前对象上查找,如果找到就直接返回(调用)并且停止查找
如果没有找到就向其原型对象上去查找,如果找到就直接返回(调用)
如果还是没有找到,就继续向原型对象的原型对象上查找,直到Object.prototype。
如果找到了,就直接返回(调用),并停止查找,否则返回undefined(报错:xxx is not a function)
注意:
-
如果访问对象的某个属性不存在的话,会搜索整个原型链,有可能会导致js性能降低。
-
在实际开发中尽量保持一个适度的原型链长度。
-
兼顾js性能以及代码的可读性和扩展性
Object.prototype介绍
constructor
就是自己的构造函数(function Object( ) { [native code] })
hasOwnProperty()
hasOwnProperty() 判断指定的属性是否为当前对象自己的(自己的就是指不是继承过来的)
构造函数里的就是对象自己的,原型上的就是继承的。
格式:obj.hasOwnProperty('属性名')
var o = {name:'tom'};
console.log(o.hasOwnProperty('name')); //返回true
console.log(o.hasOwnProperty('toString')); //返回false
isPrototypeOf()
isPrototypeOf() 用来判断当前对象是否是指定对象的原型对象
格式:obj1.isPrototypeOf(obj2)
// 只要对象A出现在B对象的原型链上就返回true,否则返回false
function A () {}
function B () {}
var a = new A;
var b = new B;
//现在没有任何关系
console.log(a.isPrototypeOf(b)); //返回false
var a = new A
B.prototype = a;
var b = new B
// b -> a -> a.prototype -> Object.prototype -> null
propertyIsEnumerable()
propertyIsEnumerable() 判断对象指定的属性是否可枚举,并且指定的属性必须是自己的,两者都满足才能返回 true
function foo(name, age, address) {
// body...
this.name = name;
this.age = age;
this.address = address;
}
foo.prototype.talk = function(){
console.log(this.name);
}
console.log(f.propertyIsEnumerable('name')); // 返回true
console.log(f.propertyIsEnumerable('talk')); // 返回false
// 可枚举就是用 for in 遍历出来的属性都是可枚举的。内置的属性(__proto__)都是不可枚举的
var obj = {
name:'tom';
age:18;
}
构造函数的执行过程
// 构造函数的执行过程
function Fn (name,age) {
this.name = name;
this.age = age;
}
var Fn = new Fn();
-
创建了一个空对象
-
将obj赋值给this (让this指向上面创建空对象,也就是Fn)
-
将当前作用域交给this
-
执行构造函数内部的代码
-
将this返回 new的时候函数内部会默认return this
网友评论