小结JS中的OOP(中)
此篇文章主要是提炼《JavaScript高级程序设计》中第六章的一些内容。
一:JS中OOP相关的概念
开始之前先总结JS中OOP相关的一些概念:
构造函数:JS中的构造函数就是普通的函数,当JS中的函数使用new调用时,这个函数就是构造函数。构造函数调用与普通函数调用相比会有以下两点不同:
① 在进入构造函数时,会先创建一个对象,并将构造函数的作用域赋值给这个对象(this指向这个对象)
② 在退出构造函数前,会默认返回创建的对象(返回this)
原型对象:每个函数都会有一个prototype属性,函数的prototype属性指向的就是(构造)函数的原型对象。默认情况下函数的原型对象都会有一个constructor属性,指向函数自身。
其实,JS中所有的对象都会有constructor属性,默认指向Object。
function foo(){} //所有的函数都具有prototype属性 //所有函数的原型对象(函数的prototype属性)默认的constructor属性指向函数自身 foo.prototype.constructor === foo; //所有对象都具有constructor属性,默认指向Object ({}).constructor === Object; //对象实例内部都会有一个[[prototype]]指针,指向对象在创建时所依赖的原型对象 (new foo()).__proto__ === foo.prototype ({}).__proto__ === Object.prototype; foo.prototype === (new foo).__proto__; (new foo).__proto__.constructor === foo;
实例: 使用new产生的一个具体的对象
二: 封装类(创建自定义类型)
使用构造函数+原型模式封装类。具体做法是:
① 把实例属性(不共享的属性)写在构造函数中
② 把共享属性(包含constructor)写在函数原型对象中
function Animal(opts) { this.cate = opts.cate; } Animal.prototype = { constructor: Animal, shout :function () { console.log('animal shouted'); } }
//为了封装看起来更优雅,也可以写成下面这样 function Animal(opts) { this.cate = opts.cate; if(typeof this.shout !== 'function') { //这样直接将一个对象赋值给Animal.prototype会断开进入构造函数时创建的对象的__proto__属性与原型对象的链接 //所以实际new出来的第一个对象的__proto__上面不会有原型对象上的方法 //解决办法是手动在后面调用一次构造函数或像下面那样增加方法就不会断开构造函数时创建的对象的__proto__属性与原型对象的链接 /** * Animal.prototype.shout = function() {} * Animal.prototype.getCate = function() {} */ Animal.prototype = { constructor: Animal, shout :function () { console.log('animal shouted'); }, getCate: function() { return this.cate; }, setCate: function(cate) { this.cate = cate; } } } } //手动new一次Animal new Animal();
这种封装的缺点:没有实现访问权限控制,所有的一切都是public的。
三:继承
使用借用构造函数+原型链接实现继承
function Animal(opts) { this.cate = opts ? opts.cate : undefined; if(typeof this.shout !== 'function') { Animal.prototype = { constructor: Animal, shout :function () { console.log('animal shouted'); }, getCate: function() { return this.cate; }, setCate: function(cate) { this.cate = cate; } } } } //手动new一次Animal new Animal(); //定义Dog类,并让其从Animal继承 function Dog(opts) { //继承Animal中的属性 Animal.call(this, opts); //增加Dog的属性 this.name = opts.name; //... } Dog.prototype = new Animal();
Dog.constuctor = Dog; Dog.prototype.getName = function() { return this.name; }
缺点很明显:Dog.prototype = new Animal(); 要实例化Dog会先调用一次Animal(),同时在new Dog()时,在Dog的构造函数中又会调用一次Animal(), Animal.call(this, opts);
由于Dog.prototype = new Animal(); 只是想拿到Animal原型对象上的方法,所以我们可以改成这样,Dog.prototype = Animal.prototype; 但改成这样后,Dog.prototype与
Animal.prototype实际就都指向同一个对象了,所以Animal的实例也会有getName()方法。但我们可以先创建一个对象,再把Animal.prototype上的属性拷贝来过,再赋值给
Dog.prototype。
function Animal(opts) { this.cate = opts ? opts.cate : undefined; } Animal.prototype = { constructor: Animal, shout :function () { console.log('animal shouted'); }, getCate: function() { return this.cate; }, setCate: function(cate) { this.cate = cate; } } //定义Dog类,并让其从Animal继承 function Dog(opts) { //继承Animal中的属性 Animal.call(this, opts); //增加Dog的属性 this.name = opts.name; //... } Dog.prototype = (function() { var DogProto = {}, AnimalProto = Animal.prototype, key; for(key in AnimalProto) { AnimalProto.hasOwnProperty(key) DogProto[key] = AnimalProto[key]; } return DogProto; })(); Dog.prototype.constructor = Dog; Dog.prototype.getName = function() { return this.name; }
这样改了后,也还是有缺点: new Dog instanceof Animal;会返回false。但如果我们只关注对象能做什么,而不是对象的类是什么(鸭式辨型编程)这样做还是达到了我们想要的效果。
还有如果我们平时想使用一些带有自己方法的原生对象,但我们又不想去直接扩展原生对象的prototype,我们可以像下面这样做:
function enhancedString(str) { str = new String(str); //增加我们的方法 !str.statsWith && (str.startsWith = function(target) { return this.indexOf(target) === 0; }); //... return str; } //测试: var str = new enhancedString('test'); //可以使用string原生的方法 console.log(str.length);//4 console.log(str.slice(1));//est //也可以使用我们在string上面扩展的方法 console.log(str.startsWith('t')); //true