javascript && 面向对象程序设计

为什么要写这一块呢哈哈,是由于在写fbx模型加载动画的时候,需要二次开发,这样就得看原始的fbx加载库,里面有众多的继承等知识。所以,js的面向对象的知识如果可以掌握透彻,那么看源码的能力也可以大大提升啦,看懂并自己扩展功能,是一件很让人兴奋的事情呢^_^

属性

创建对象,就是要访问里面的属性,那么,除了数据类型外,属性还分为哪几种呢?答案是:数据属性&&访问器属性,它们都有自己的特性

  • 数据属性:

configurable:能否使用delete删除、能否改变属性特性、能否改为访问器属性,默认值是true
enumerable:能否通过for in枚举,默认值true
writable:能否重写属性的value,默认值true
value:属性的值,默认undefined

知道了这些属性后,怎么在实际的开发中操作这些特性呢?下面我们就来看一下这个函数:
Object.definePropertype();
它接收三个参数:操作的对象、操作的属性名、描述符对象(包含的属性就是上面介绍的四种特性)

var person = {name: "lyn"};
Object.defineProperty(person, name, {
configurable: true;
writable: false;
});
person.name = "k";
console.log(person.name);   //lyn

注意:
(1)每次使用Object.defineProperty()修改属性特性时,如果不指定,configurable、enumerable、writable默认值都是false
(2)configurable修改为false后,再修改除了writable之外的特性,都会报错。

  • 访问器属性

访问器属性必须通过Object.defineProperty()来定义,不可以直接定义,主要的用途就是:读取此属性时,可以执行指定的函数,所以可以改变一个访问器属性的同时,可以改变多个数据属性。
四个特性:
configurable:能否使用delete删除、能否改变属性特性、能否改为访问器属性,默认值是true
enumerable:能否通过for in枚举,默认值true
get:读取该属性时,调用的函数,默认undefined
set:写入该属性时,调用的函数,默认undefined

var book = {
	_year: 2004,
	edition: 1
}
Object.defineProperty(book, "year", {
get: function() {
	return this._year
},
set: function(newValue) {
	if(newValue > 2004){		
		this.edition += (newValue - this._year);
		this._year = newValue;
	}
}
});
book.year = 2005;
console.log(book);       //{_year: 2005, edition: 2}

注意:只指定get表明该访问器属性不能写,只指定set表明该访问器属性不能读

  • 定义多个属性的方法:
    在实际开发中,我们经常需要为对象定义多个属性,ES5提供了这样的一个函数,可以一次性定义多个参数,同时可以定义它们的特性:
    Object.defineProperties(obj1, obj2);
    obj1是要修改的对象,obj2是描述符对象,里面包含各个属性的描述符对象,注意要与原来的对象的属性一一对应
  • 读取属性的方法
    知道了如何定义属性的特性,那怎么读取这些呢?
    Object.getOwnPropertyDescriptor();
    接收两个参数,第一个是对象,第二个是想要获取的属性;
    返回值:描述符对象d,可以使用d.configurable来查看特性

了解对象的属性之后,要想知道js中的继承是怎么使用的,那我们必须得先知道对象是怎么创建的,之后才能了解对象与对象之间的关系。通俗一点说对象中的原型,其实就是为了查找对象的属性,在本实例中找不到,就去原型对象里面找。

对象的创建

对象的创建模式众多,其实模式也就是各种创建对象的方法,每种方法呢还都有自己的优缺点,模式的演变也是开发者不断找寻更好的使用方法的结果

  • 工厂模式
    很简单,就是把常见的使用new Object()的方法创建的对象封装在一个函数中,只有创建对象只需要调用这个韩式即可。虽然解决了代码冗长的问题,但是无法区分函数的类型,因为都是Object

  • 构造函数模式

 function Person(name, age) {
 	this.name = name;
 	this.age = age;
 	this.getName = function (){
		return this.name;
	}
 }
 var person1 = new Person("lyn", 23);

使用new创建实例,过程:
先创建一个对象,再将this指向新的对象,属性方法赋值,返回此对象
利用实例的构造函数属性,即person1.constructor == Person可以知道实例的类型,这点胜于工厂模式
缺点在于创建多个完成同样任务的实例的方法确实没有必要,如上所述的getName函数,但是把这样的函数放到全局作用域一方面污染全局作用域,一方面还损失了类型的封装性。这样就想到了原型模式。

  • 原型模式
    解决构造函数的缺点的方法就是:将上述的实例的共有的方法放在一个所有实例都可以共享的的对象中,这个对象还不会污染全局环境。那么,构造函数的prototype属性,就可以完成这个任务啦~
    每个构造函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个共享区域(就是一个对象啦),所有基于此构造函数创建的实例都可以共享这个共享区域的属性和方法,这个共享区域就是实例的原型对象。
var function Person() {
	this.name = "lyn";
}
Person.prototype.getName = function(){   //实例的原型对象
	alert(this.name);
}
var person1 = new Person();
person1.getName();    //lyn

构造函数、原型对象、实例之间的关系
上图就是构造函数、原型对象、实例之间的关系
创建了构造函数后,就会根据一组规则去指定一个prototype属性,这个属性指向它的原型对象。默认情况下这个对象会获得一个constructor属性指向构造函数(拥有prototype属性的函数),通过构造函数的prototype属性,可以为原型对象添加属性和方法等等,这也是实例们共有的属性和方法。
当调用构造函数创建实例后,这个实例内部会有一个指针,指向构造函数的原型对象,该指针是内部属性[[Prototype]],一般访问不到,个别浏览器支持使用__proto__来访问这个指针。注意,这个连接仅存在于实例和原型对象上,与构造函数没有关系,确定实例和原型对象之间的关系有两个函数:
(1)构造函数.prototype.isPrototypeOf(实例); //返回布尔值
(2)Object.getPrototypeOf(实例); //返回实例的原型对象

读取实例的属性时,先在实例本身搜索,找不到的话就会去它的原型对象里面搜索,这也是实例们可以共享属性和方法的原因,因为它们都有指向同一个原型对象的指针。在此处需要注意:
(1)原型对象的属性不会被实例改写,就算是实例中定义了一个属性和原型对象重名,也只能标识符解析不向原型对象搜索,当delete重名实例属性后,还是会向原型对象搜索。
(2)谈到了属性,这里要提一下,还记得总结对象属性的时候提到的Object.defineProperty()吗?这个函数只能用于实例属性,若是想设置原型对象属性的特性,需要传参原型对象,传实例是不能改变原型对象的属性特性的。
那么盆友们肯定会想,我咋知道摆在我面前的属性是不是原型对象里面的呢?这个时候,如下函数就排上用场了~
实例对象.hasOwnProperty(属性);         //如果是实例属性,返回true
原型与in操作符:
(1)"name"  in person1;    //返回布尔值,无论是实例还是原型中的属性,都能访问到
(2)for(var  prop  in   person1) //遍历每个属性,能够访问到实例属性和原型中可以枚举的属性
可以使用person1.hasOwnProperty(属性)和in操作符来判断一个属性是不是原型对象的属性
(3)Object.getOwnPropertyNames(实例对象); //包含着所有实例属性,包括不可枚举的
(4)Object.keys(实例对象)://返回字符串数组,包含着可枚举的实例属性
简洁的原型写法:
添加原型属性时频繁敲“Person.prototype…”,为了减少这种不必要的代码量,可以重写Person.prototype = {name: xxx,age:xxx};不要忘记重写的时候要改变对象的constructor属性,并将其enumerable特性设为true。
原型的动态性:
指的就是创建实例之后,再改变原型的属性和方法,之后再通过实例访问原型属性和方法的时候,依然可以正常访问,这是由于原型对象和实例之间的松散的连接关系。但是如果创建实例之后改变的不是属性和方法,而是重写了整个原型对象(constructor指向正确的构造函数的情况下),那实例就不可以访问重写之后的原型对象的属性和方法了,因为实例里面的__proto__指向的是最初的原型对象,指针值不会发生变化,这时,使用instanceof也会是false。
不过若是先重写原型,后创建实例,那么instanceof还是true。原理如下图所示:
原型动态中的instanceof单独使用原型模式的缺点:(1)没有初始化参数,(2)最大的问题:共享机制导致的修改一个实例的原型属性A,其他实例的原型属性A也跟着被修改了,对象总是要有自己的特色的,所以要组合使用构造函数模式和原型模式

  • 组合构造函数模式和原型模式
    这个模式是使用最广泛的一种模式
    将实例特有的属性放在构造函数里,把实例共享的属性和方法放在原型里:
function Person(name, age) {
	this.name = name;
	this.age = age;
}
Person.prototype = {
	constructor: Person;
	getName: function() {
		alert( this.name );
	}
};
  • 动态原型模式
    原型对象的定义放在构造函数中
function Person(name, age) {
	this.name = name;
	this.age = age;
	if(typeof this.getName != "function") {
		alert("我只执行一次!");
		Person.prototype.getName = function() {
			alert(this.name);
		}		
	}
}
var p1 = new Person("lyn", 23);
var p2 = new Person("ww", 3);

第一次创建实例的时候,定义了原型对象中的getName的方法,当第二此new一个实例的时候,在判断this.getName是不是一个function时,由于this指向的是p2,在实例中的方法找不到的时候,在搜索原型对象中找到了,所以就不会再执行一次if了,所以也就只定义了一次原型,同时还把构造函数和原型对象合并在了一起。

  • 寄生构造函数模式
    小众的一种模式,用法是使用工厂模式创建函数,不过这个函数叫做构造函数,创建实例的时候,使用new操作符。常用于为一些对象(比如数组)添加特殊的方法,由于不能更改Array构造函数,所以就可以使用这个模式创建一个特殊的数组类型。注意,这种模式创建出来的实例与构造函数和构造函数的原型没有任何关系。和工厂模式区别就在于使用new+构造函数,创建出的实例时等价的。
    使用寄生构造函数模式比工厂模式看起来更像是创建一个对象,而不是简单地调用一个函数。
  • 稳妥构造函数模式
    相对于寄生,去掉了this和new,没有公共属性,方法中不使用this引用。适用于安全环境(不允许使用this和new),或者防止其他应用程序访问。
    这种模式简单说就是私有属性只有通过内部的公共方法才能访问
function Person(name, age) {
	var o = {};
	var name = name;
	var age = age;
	var getName = function() {
		alert(name);
	}
	o.getAge = function() {
		alert(age);
	}
	return o;
}
var p1 = Person(2, 4);
Person.name;  //不是2,是Person函数名

使用函数的局部作用域以及闭包的概念,实现了数据保护的机制。在构造函数执行完毕后,内部变量本应该被销毁,但是由于返回了闭包函数,所以构造函数内部的局部变量没有被销毁,但是,只有通过公共方法(getAge)才能访问构造函数内部的属性。使用Person.name不能访问,因为name是局部变量,不是对象的属性。

继承

  • 原型链
    将构造函数A的原型重写为另一个构造函数B的实例,这样,A就继承了B的实例和原型中包含的属性,A的原型对象(也是B的实例)和B的原型对象就构成了原型链(OOP)。找对象的属性和方法的时候,标识符解析也是沿着原型链来的。
    实现OOP的基本模式
    所有原型默认都继承自Object,如果没有指定原型对象,那其原型对象就是Object
    原型和实例之间的关系:
    (1)实例  instanceof   构造函数
    原理就是检测构造函数的原型是不是存在于实例的原型链中
function instanceofNew(shi, construct) {
	while(Object.getPrototypeOf(shi) != null) {
		if(Object.getPrototypeOf(shi) == construct.prototype) {
			return true;
		}
		shi = Object.getPrototypeOf(shi);
	}
	return false;
}

(2)构造函数.prototype.isPrototypeOf(实例) //返回布尔值,结果和instanceof一样
子类型重写父类型中的方法,或者在子类型中添加父类型不存在的方法时,要记得先实现继承(将子类型的原型赋值为父类型的实例),再定义方法,否则就算是定义了方法,之后再继承的时候,原型对象指向了新的父类型的实例,之前定义的方法会找不到,因为之前定义方法的那个对象已经不在原型链中了。
原型链继承存在的问题是:
(1)父类型实例属性传到子类型的原型对象后,子类型实例共享,也就是子类型的实例改变了其中一个值,那其他实例们都将受影响
(2)子类型无法向父类型的构造参数传参,
传参的意义:比如说有一个动物父类,狗子类,父类的实例属性有一个是name,每个子类型都要有name属性,但是值都不同,这个时候就需要传参啦~~

  • 借用构造函数

指的是在子类型的构造函数中,将父类型的实例属性、实例方法复制一份,以便于将来传递给子类型的实例:这样,既可以解决无法传参的问题,又可以防止父类型的实例属性方法共享的问题。
思想就是在子类型的构造函数中调用父类型的构造函数(直接调用,不使用new),并使用call或者apply来指定在子类型的实例中运行代码。

function Person(name) {
	this.name = name;
	this.colors = ["red", "blue"];
}
function Sub (name) {
	Person.call(this, name);
} 
var sub1 = new Sub("lyn");
sub1.colors.push(1);  //sub1.colors: ["red", "blue", 1];
var sub2 = new Sub("s"); //sub2.colors: ["red", "blue"];不受改变

但是这样的方法创建的子类型实例,与父类型的原型对象没有丝毫关系,也就是说所有的方法都需要放在构造函数中。所以为了使用原型对象,原型链继承和构造函数结合形成了新的继承方式:组合继承

  • 组合继承
    原型链继承借用构造函数继承的优点于一身
    通过原型链来继承原型对象的属性和方法;借用构造函数来实现对实例属性的继承。这样既可以使用原型链来保证函数的复用,也可以通过构造函数来保证每个实例具有自己的属性。
    将超类(父类型)的实例属性方法使用“call+调用超类构造函数方式”复制到子类实例中,将超类实例赋值给子类的原型,实现原型对象之间的连接。
function Super(name) {
	this.name = name;
	this.colors = ["red", "blue", "yellow"];
}
Super.prototype.getName = function() {
	return this.name;
}
function Sub(name, age) {
	Super.call(this, name); //继承属性
	this.age = age;
}
Sub.prototype = new Super();
Sub.prototype.getAge = function() {
	return this.age;
}
var sub1 = new Sub("lyn", 22);
var sub2 = new Sub("lr", 21);

组合继承是最常用的方式,通过instanceof可以判断类型
疑问:每次new一个子类型的实例时,都要复制一次超类的实例属性,这样我觉得很浪费时间和内存,有什么改进方法吗????????????欢迎评论

  • 原型式继承(不考虑自定义类型和构造函数的情况下考虑)
    基于指定的一个字面量对象,在其基础上进行改造,得到以此字面量对象为原型对象的一个实例。对于简单的继承,不需要兴师动众去创建构造函数,可以使用这种方式。类似于{…obj}的一种浅复制。
function object(o) {
	function Fun() {}
	Fun.prototype = o;
	return new Fun();
}
var person = {name:"lun", age:[1, 2]};
var another = object(person);
//得到的another对象就是一个以person为原型对象的实例
//改变another的基本数据属性,不会影响person;
//但是改another里面的引用类型,就会影响person。
var another = {...person}//结果类似

ES5规范了原型式继承,Object.create(obj1,{})
当只传入一个参数的时候,和上述object函数功能一样
第二个参数是描述符对象:

var another = Object.create(person, {
	name: "newName"
});
another;    //{name: newName, age: [1, 2]}注意name的值变化了
  • 寄生式继承(不考虑自定义类型和构造函数的情况下考虑)
    原型式继承的基础上为增强新对象,即为得到的新对象添加属性或者方法,如下面的clone.get中的get
function createNewObject(obj) {
	var clone = Object.create(obj); //和前述object()函数一个作用
	clone.get = funxtion(){
		alert(this);
	}	
}

不仅继承了基础对象的属性,还拥有了自己的方法.(添加的自己的函数无法得到复用,会降低效率,这一点是和构造函数模式一样的问题)

  • 寄生组合式继承(被程序员看成是最理想的继承方式)
    组合继承寄生式继承的优点于一身
    改良组合继承中的一个不足之处:
    要知道,每次new一个构造函数,那构造函数内部的实例属性就会被添加到实例的原型对象中。那么SubType.prototype = new SuperType来实现原型对象之间的继承时,子类型的原型对象就有了超类的实例属性,其实这是很没有必要的,因为原型存的是公共的方法和属性,实例属性已经使用SuperType.call(this)来继承了,那么子类型的原型对象接收到的超类的实例属性就是内存上的无用占用,为了解决这个问题,使用什么方法可以替换SubType.prototype = new SuperType,也能达到原型对象之间的继承?
    关键在于子类型的原型指向,
    第一条:不是超类的实例(这样才可以不接收无用的实例属性);
    第二条:子类型的原型可以指向超类的原型对象(这里要注意,不可以直接SubType.prototype = SuperType.prototype,因为父子共用一个原型对象,首先失去了每个类型各自的特色,其次要实现继承的话,不利于扩展子类型原型的方法属性等等,还有就是这样的话就失去了原型对象之间的链式结构)
    还记得原型式继承吗?这种方式就根据一个对象返回一个以它为原型对象的一个实例,那么我们不就可以根据寄生式继承(相较于原型式继承:可以增强对象)得到子类型的原型啦~
function Super(name) {
	this.name = name;
	this.colors = [1,2,3,4];
}
Super.prototype.getName = function() {
	return this.name;
}
function Sub(name, age) {
	Super.call(this, name);   //继承超类实例属性
	this.age = age;           //添加子类实例属性
}
function inheritPrototypeObject(subObj, superObj) {
	var subPrototypeObject = Object.create(superObj.prototype);
	subPrototypeObject.constructor = subObj;   //增强对象,防止指向Super(Super:顺着原型链找的)
	return subPrototypeObject;
}
Sub.prototype = inheritPrototypeObject(Sub, Super);
//Sub.prototype = new Super();此语句被上句替换
Sub.prototype.getAge = function() {
	return this.age;
}
var p1 = new Sub("lyn", 3);
var p2 = new Sub("e", 5);
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值