说起面向对象,大部分程序员首先会想到 类 。通过类可以创建许多具有共同属性以及方法的实例或者说对象。但是JavaScript并没有类的概念,而且在JavaScript中几乎一切皆对象,问题来了,JavaScript中如何面向对象?
JavaScript中将对象定义为:一组无序的 键值对的集合,属性以及方法的名称就是键,键的值可以是任何类型(字符串,数字,函数……)
在JavaScript中,所有对象继承自Object,所有对象继承自Object,所有对象继承自Object!
创建
1 简单创建对象:
使用上面的方法虽然可以创建对象,但缺点也很明显,假如再次创建一个具有相同属性以及方法的对象,还得把代码复制修改一遍,会产生大量的重复代码。
2 工厂模式
这种模式抽象了创建对象的具体过程
function createperson(name,age){ var o = new Object(); o.name = name; o.age = age; o.sayName = function(){ alert(this.name); }; return o; } var p1 = createperson('mncu',120); var p2 = createperson('linghuchong',120); p1.sayName(); //'mncu' p2.sayName(); //'linghuchong'
console.log(typeof p1); //object
p1 instanceof createperson // false ,无法判断类型
但这种方式有一个缺点:无法判断某个对象是什么类型。
3 构造函数模式
function Person(name,age){ // 按照惯例,构造函数的首字母要大写 this.name = name; this.age = age; this.sayName = function(){ alert(this.name); }; } var p1 = new Person('mncu',120); // 必须使用new关键字创建对象 var p2 = new Person('linghuchong',120); p1.sayName(); //'mncu' alert(p1 instanceof Person); // true,可判断类型
构造函数模式解决了工厂模式中不能判断对象类型的问题。
在使用构造函数模式时要注意:
--必须使用new关键字创建对象! new 关键字相当于做了以下几步:
1 创建一个新对象
2 将构造函数的作用域赋值给这个新对象(因此this就指向了这个新对象)
3 执行构造函数的代码(为这个新对象添加属性)
4 返回新对象
--使用构造函数创建的对象都有一个constructor属性,该属性可以标识对象类型,但一般我们还是常用instanceof来判断对象类型, 因为constructor属性仅返回构造函数类型。
p1.constructor === Person //true
p1.constructor === Object //false
p1 instanceof Object // true p1 instanceof Person //true
--构造函数实际上和普通函数并无多大区别,因为其不存在特殊的语法,任何函数,只要通过new调用,那么他就可以作为构造函数。
var p3 = Person('dongfangbubai',120); // 不使用new时,函数的作用域会是全局作用域,this就会指向window对象。 p3.sayName() //报错 sayName() // 'dongfangbubai'
--构造函数也存在问题,每个方法都要在实例上创建一遍。也就是说p1和p2的sayName()方法虽然作用相同,但这两个方法并不是同一个函数。
p1.sayName == p2.sayName // false
--无new创建:
function Person(name,age){ //alert(this); if(!(this instanceof Person)){ return new Person(name,age); } this.name = name; this.age = age; this.sayName = function(){ alert(this.name); } } var p1 = Person('hah',20); console.log(p1.name)
4 原型模式
原型模式解决了构造函数模式中同功能的方法的代码无法重用的问题。
我们创建的每个函数都有一个名为prototype的属性,这个属性是一个指针,指向一个对象,这个对象被称为原型对象。原型对象有一个名叫constructor的属性,这个属性是一个指针,指向构造函数。默认情况下,所有函数的原型都是Object的实例。
使用原型模式创建对象:
function Person(){ Person.prototype.name = 'noOne'; Person.prototype.age = 'noAge'; Person.prototype.sayName = function(){ return this.name; }; } var p1 = new Person(); p1.name; // 'noOne'
var p2 = new Person()
p2.name; //'noOne'
在本例中,构造函数创建的对象p1的name属性是如何展现的?
首先p1会查找自身有没有name属性,如果有的话,就返回自身的name属性的值,如果没有的话,则查找原型对象中有没有name属性,若原型对象中有name属性,则返回其值,否则,就报错。
p1.name = 'mncu'; p1.name; // 'mncu' p2.name; //'noOne'
我们可以通过hasOwnProperty()方法检测一个属性是存在于具体的对象中,还是存在于该对象的原型对象中
p1.hasOwnProperty('name') //true p2.hasOwnProperty('name') //false
我们也可以通过 in 关键字来判断一个属性是否存在于具体的对象或者该对象的原型对象中
'name' in p1 // true 'name' in p2 // true
原型对象的简写格式:
function Person(){ } Person.prototype={ // 将原型对象重写,但重写后原型对象的constructor就不会指向Person了(指向Object)。所以我们一般会添加:constructor:Person constructor:Person, name:'noOne', age:'noAge', sayName:function(){ alert(this.name); } };
原型模式存在的问题:
function Person(){ } Person.prototype={ constructor:Person, name:'noOne', age:'noAge', brothers : ['xiaoming'], sayName:function(){ alert(this.name); } }; var p1 = new Person(); var p2 = new Person(); p1.brothers.push('xiaohong'); console.log(p2.brothers) // ['xiaoming','xiaohong']
当我们改变 值为引用类型的对象的属性 时,这个改变的结果会被其他对象共享。
5 将构造函数模式和原型模式结合
构造函数模式的属性没毛病。缺点是:无法共享方法
原型模式的方法没毛病。缺点是:当原形对象的属性的值为引用类型时,对其进行修改会反映到所有实例中
那我们就将两者的结合,对象的属性使用构造函数模式创建,方法则使用原型模式创建
这种方式是最为常见的一种面向对象编程模式
function Person(name,age){ this.name = name; this.age = age; } Person.prototype={ constructor:Person, sayName:function(){ alert(this.name); } }; var p1 = new Person('mncu',120); p1.name; // 'mncu'
继承
1 原型链
JavaScript中引入了原型链的概念,具体思想: 子构造函数的原型对象初始化为父构造函数的实例,孙构造函数的原型对象初始化为子构造函数的实例…… ,这样子对象就可以通过原型链一级一级向上查找,访问父构造函数中的属性以及方法。
构建原型链:
原型链继承的问题:
function SuperObject(){ this.colors = ['red','blue']; } function SubObject(){ } SubObject.prototype = new SuperObject(); var instance1 = new SubObject(); instance1.colors.push('yellow'); var instance2 = new SubObject(); console.log(instance2.colors) // ["red", "blue", "yellow"]
当我们改变 值为引用类型的原型对象的属性 时,这个改变的结果会被所有子对象共享。这个缺点某些时候相当致命,所以我们很少使用这种方法来继承
2 借用构造函数继承
function SuperObject(){ this.colors = ['red','blue']; this.sayBye= function(){ console.log('Bye') } } function SubObject(){ SuperObject.call(this); // 在子类中调用父类的构造方法,实际上子类和父类已经没有上下级关系了 } var instance1 = new SubObject(); instance1.colors.push('yellow'); var instance2 = new SubObject(); console.log(instance2.colors); //['red','blue'] console.log(instance2 instanceof SuperObject); // false console.log(instance1.sayBye === instance2.sayBye) // false
这个方法虽然弥补了原型链的缺点,但是又暴露出了新的缺点:
1 子类和父类没有上下级关系,instance2 instanceof SuperObject 结果是false
2 父类中的方法在每个子类中都会生成一遍,父类中的方法没有被复用。
3 组合继承
组合继承就是将原型链继承和借用构造方法继承组合,发挥两者之长。
function SuperObject(){ this.colors = ['red','blue']; } SuperObject.prototype.sayBye= function(){ console.log('bye') }; function SubObject(){ // 引用父类型的属性,又调用了一次父函数 SuperObject.call(this); } // 继承父类型的方法,调用了一次父函数 SubObject.prototype = new SuperObject(); var instance1 = new SubObject(); instance1.colors.push('yellow'); var instance2 = new SubObject(); console.log(instance2.colors); //['red','blue'] console.log(instance2 instanceof SuperObject); // true console.log(instance1.sayBye === instance2.sayBye); // true
4 寄生组合式继承----道格拉斯方法
虽然组合继承没啥大缺点,但是爱搞事情的有强迫症的程序猿们觉得,组合继承会调用两次父类型函数(在上面的代码中标注了),不够完美。于是道格拉斯就提出了寄生组合继承。
思路是构造一个中间函数,将中间函数的prototype指向父函数的原型对象,将子函数的prototype指向中间函数,并将中间函数的constructor属性指向子函数。
function inherits(Child, Parent) { var F = function () {}; F.prototype = Parent.prototype; Child.prototype = new F(); Child.prototype.constructor = Child; } function Student(props) { this.name = props.name || 'Unnamed'; } Student.prototype.hello = function () { alert('Hello, ' + this.name + '!'); } function PrimaryStudent(props) { Student.call(this, props); this.grade = props.grade || 1; } // 实现原型继承链: inherits(PrimaryStudent, Student); // 绑定其他方法到PrimaryStudent原型: PrimaryStudent.prototype.getGrade = function () { return this.grade; };
这个方法只调用了一次父构造函数,并因此避免了在父函数的原型对象上创建不必要的、多余的属性。
开发人员都表示:这种方法是最理想的继承方式(我没看出来,只觉得这是最烧脑的继承方式,看来我离开发人员还有一定距离。。。)