【读书笔记】读《编写高质量代码—Web前端开发修炼之道》 - JavaScript原型继承与面向对象
JavaScript是基于原型的语言,通过new实例化出来的对象,其属性和行为来自于两部分,一部分来自于构造函数,另一部分是来自于原型。构造函数中定义的属性和行为的优先级比原型中定义的属性和优先级高,如果构造函数和原型定义了同名的属性和行为,构造函数中的属性和行为会覆盖原型中的同名的属性和行为。如下图——
当我们声明一个类时,其实同时生成了一个对应的原型,例如我们定义Animal这个类时,会生成一个与Animal类对应的原型,通过Animal.prototype可以指向这个原型,原型可以通过constructor指向Animal类,更确切的说,是指向Animal类的构造函数。
function Animal() {} var a = Animal.prototype; b = a.constructor; alert(b === Animal); //true
//**************************** 1.开始应用原型 **************************** (function() { function Animal() {} // 旧的方式 // function Animal(name) { // this.name = name; // this.type = 'animal'; // this.say = function () { // alert('Hello'); // } // } Animal.prototype = { name : 'king', say : function() { //this.name和this.type均能访问到,与它们属性所在的先后顺序是无关的 console.log('I am a ' + this.type + ', my name is ' + this.name); }, type : 'animal' } //-----test----- var dog = new Animal(); dog.say(); //I am a animal, my name is king })();
//**************************** 2.属性与方法的分离 **************************** (function() { //我们习惯把属性放在构造函数里面 function Animal(name) { this.name = name || 'king'; this.type = 'animal'; } //将方法放在类的原型里 Animal.prototype = { say : function() { console.log('I am a ' + this.type + ', my name is ' + this.name); } } //-----test----- var dog = new Animal(); dog.say(); //I am a animal, my name is king })();
//**************************** 3.公有和私有 **************************** //用this.xxx定义的属性是公有的,用var xxx定义的属性是私有的 (function() { function Animal(name) { this.name = name || 'king'; this.type = 'animal'; //私有变量 var age = 20; //私有方法 var move = function() { alert('I am a moving dog'); } } Animal.prototype = { say : function() { console.log('I am a ' + this.type + ', my name is ' + this.name); } } //-----test----- var dog = new Animal('wangcai'); console.log(dog.age); //undefined dog.say(); //I am a animal, my name is wangcai dog.move(); //报错,has no method 'move' })();
//**************************** 4.如何访问到私有变量(方法) **************************** (function() { function Animal(name) { this.name = name || 'king'; this.type = 'animal'; //私有变量 var age = 20; //私有方法 var move = function() { alert('I am a moving dog'); } //在构造器作用域(私有变量的作用域)中,构造一个能够访问私有变量的公有方法 this.say = function() { console.log('I am a ' + this.type + ', my name is ' + this.name + ', my age is ' + age); } this.act = function() { move(); } } Animal.prototype = {}; //-----test----- var dog = new Animal('wangcai'); dog.say(); //I am a animal, my name is wangcai, my age is 20 dog.act(); //I am a moving dog /** * 将所有属性和行为,无论公有还是私有的属性和行为全部写在构造函数里,的确是最方便的方式,但并不推荐这么做。 * 因为在内存中一个类的原型只有一个,写在原型中的行为,可以被所有实例所共享,实例化的时候, * 并不会在实例的内存中再复制一份,而写在类里的行为,实例化的时候会在每个实例里复制一份。 * 把行为写在原型里可以减少内存消耗,没有特殊原因,推荐尽量把行为写在原型里。 */ })();
//**************************** 5.如何访问到私有变量(方法) —— 性能改造**************************** /** * 写在原型里的行为一定是公有的,而且无法访问私有属性,所以如何处理私有行为和私有属性是个难题。 * 1>如果对属性和行为的私有性有非常高的强制性,我们不得不牺牲内存,将私有行为放在构造函数里,实现真正的私有。 * 2>如果对属性和行为的私有性要求不高,更常见的做法是约定私有行为, * 通过给属性和行为的名称前面加上“_”来约定它是私有的,这是一种命名约定。 */ (function() { function Animal(name) { this.name = name || 'king'; this.type = 'animal'; //私有变量 this._age = 20; } Animal.prototype = { _move : function() { alert('I am a moving dog'); }, say : function() { //访问this._age来标识age属性是私有的 console.log('I am a ' + this.type + ', my name is ' + this.name + ', my age is ' + this._age); }, act : function() { //访问this._age来标识move方法是私有的 this._move(); } }; //-----test----- var dog = new Animal('wangcai'); dog.say(); //I am a animal, my name is wangcai, my age is 20 dog.act(); //I am a moving dog console.log(dog._age); //不推荐实例直接调用_age,违反命名约定 dog._move(); //I am a moving dog 不推荐实例直接调用_move,违反命名约定 })();
//**************************** 6.继承:继承构造函数 **************************** (function () { function Animal(name) { //应用函数调用模式,此时的this指向window this.name = name || 'king'; this.type = 'animal'; } Animal.prototype = { say: function () { console.log('I am a ' + this.type + ', my name is ' + this.name); } } function Bird(name) { //此时的this是Bird Animal(name); //这个方法调用过后this就变成了window(函数调用模式) } //-----test----- var bird = new Bird('xiaocui'); console.log(bird.type); //undefined,此时的bird对象没有任何自身属性 })();
//**************************** 7.继承:继承构造函数 —— 改造 **************************** (function () { function Animal(name) { //此时的this指向Bird this.name = name || 'king'; this.type = 'animal'; } Animal.prototype = { say: function () { console.log('I am a ' + this.type + ', my name is ' + this.name); } } //这里我们只对构造器进行了继承操作,但对于Animal类里面的方法却无法继承过来 function Bird(name) { //此时的this是Bird Animal.call(this, name); //利用call方法改变作用域 } //-----test----- var bird = new Bird('xiaocui'); console.log(bird.type); //animal bird.say(); //报错,has no method 'say' })();
//**************************** 8.继承:继承原型 —— 1 **************************** //方式1:Bird.prototype = Animal.prototype; (function () { function Animal(name) { //初始化完这个函数后Animal.prototype.contructor === Animal; //true //此时的this指向Bird this.name = name || 'king'; this.type = 'animal'; } //对Animal.prototype重新赋值,那么此时的Animal.prototype.contructor === Animal; //false Animal.prototype = { say: function () { console.log('I am a ' + this.type + ', my name is ' + this.name); } } function Bird(name) { Animal.call(this, name); } //原型继承 Bird.prototype = Animal.prototype; //-----test----- var bird = new Bird('xiaocui'); console.log(bird.type); //animal bird.say(); //I am a animal, my name is xiaocui })();
//**************************** 9.继承:继承原型 —— 2 **************************** //方式1:Bird.prototype = Animal.prototype; //给子类增加独有方法,如给Bird类增加fly方法 (function () { /** * Animal.prototype = { //Object * constructor: funciton Animal(name) {...}, * __proto__: Object * } */ function Animal(name) { this.name = name || 'king'; this.type = 'animal'; } /** * Animal.prototype = { //Object * say: funciton () {...}, * __proto__: Object * } */ Animal.prototype = { say: function () { console.log('I am a ' + this.type + ', my name is ' + this.name); } } /** * Bird.prototype = { //Object * constructor: funciton Bird(name) {...}, * __proto__: Object * } */ function Bird(name) { Animal.call(this, name); } /** * 执行完这一句,Bird.prototype与Animal.prototype指向同一个对象,即 * Bird.prototype = Animal.prototype = { //Object * say: funciton () {...}, * __proto__: Object * } */ Bird.prototype = Animal.prototype; /** * 执行完这一句,增加Bird.prototype的属性(fly方法),即 * Bird.prototype = Animal.prototype = { * fly: function () {...}, * say: function () {...}, * __proto__: Object * } */ Bird.prototype.fly = function () { console.log('I am flying'); } //-----test----- var bird = new Bird('xiaocui'); bird.fly(); //I am flying var dog = new Animal('king'); dog.fly(); //I am flying /** * 由于Bird.prototype和Animal.prototype指向同一个地址,对Bird.prototype新增的方法, * 通过对Animal.prototype的属性访问也能够访问到Bird.prototype新增的方法。 * 显然,这并不是我们想要的。 */ })();
//**************************** 10.继承:继承原型 —— 3 **************************** //方式二:Bird.prototype = new Animal(); //给子类增加独有方法,如给Bird类增加fly方法 (function () { function Animal(name) { this.name = name || 'king'; this.type = 'animal'; } Animal.prototype = { say: function () { console.log('I am a ' + this.type + ', my name is ' + this.name); } } function Bird(name) { Animal.call(this, name); } /** * 执行完这一句,把Animal原型中的say方法继承过来,即 * Bird.prototype = { * name: 'king', * type: 'animal', * __proto__: { * say: function () {...} * } * } */ Bird.prototype = new Animal(); /** * 执行完这一句,重新赋值Bird.prototype的构造器属性,即 * Bird.prototype = { * constructor: function Bird(name) {...}, * name: 'king', * type: 'animal', * __proto__: { * say: function () {...} * } * } */ Bird.prototype.constructor = Bird; /** * 执行完这一句,增加Bird.prototype的属性(fly方法),即 * Bird.prototype = { * constructor: function Bird(name) {...}, * fly: function () {...}, * name: 'king', * type: 'animal', * __proto__: { * say: function () {...} * } * } */ Bird.prototype.fly = function () { console.log('I am flying'); } //-----test----- var bird = new Bird('xiaocui'); bird.say(); //I am a animal, my name is xiaocui bird.fly(); //I am flying var dog = new Animal('king'); dog.fly(); //报错,has no method 'fly' })();
//**************************** 11.继承:封装继承函数 **************************** (function () { function extend(subClass, superClass) { var F = function () {}; //构建中间量 F.prototype = superClass.prototype; //中间量原型指向父类原型 subClass.prototype = new F(); //将父类原型赋值给子类原型,能够继承所有父类原型的方法 subClass.prototype.constructor = subClass; //子类原型构造器属性指回子类构造器,使得子类的方法不会新增或覆盖父类的方法 subClass.superclass = superClass.prototype; //给子类添加superclass属性,并将其赋值父类原型 //使得在子类构造器中成功利用superclass属性来调用父类构造器 if (superClass.prototype.constructor === Object.prototype.constructor) { superClass.prototype.constructor = superClass; //重新赋值父类原型构造器为父类 } } function Animal(name) { this.name = name || 'king'; this.type = 'animal'; } Animal.prototype = { say: function () { console.log('I am a ' + this.type + ', my name is ' + this.name); } } function Bird(name) { //具备通用性 this.constructor.superclass.constructor.apply(this, arguments); } extend(Bird, Animal); Bird.prototype.fly = function () { alert('I am flying'); } //-----test----- var bird = new Bird('xiaocui'); bird.say(); //I am a animal, my name is xiaocui bird.fly(); //I am flying })();
//**************************** 12.面向过程与面向对象 **************************** /** * 面向过程编程 * 1.这种编程方式将程序分成了“数据”和“处理函数”两个部分,程序以“处理函数”为核心, * 如果要执行什么操作,就将“数据”传给相应的“处理函数”,返回我们需要的结果。这种编程方式就是面向过程编程。 * 2.这种编程方式存在的问题如下: * 1> 数据和处理函数之间没有直接的关联,在执行操作的时候,我们不但要选择相应的处理函数, * 还要自己准备处理函数需要的数据,也就是说,在执行操作的时候,我们需要同时关注处理函数和数据。 * 2> 数据和处理函数都暴露在同一个作用域内,没有私有和公有的概念,整个程序中所有的数据和处理函数都可以相互访问, * 在开发阶段初期也许开发速度很快,但到了开发后期和维护阶段,由于整个程序耦合得非常紧, * 任何一个处理函数和数据都有可能关联到其他地方,容易牵一发而动全身,从而加大了修改难度。 * 3> 面向过程的思维方式是典型的计算机思维方式——输入数据给处理器,处理器内部执行运算,处理器返回结果。 * 也就是说面向过程的思维方式是在描述一个个“动作”。 * 而现实生活中的一个个“物件”(如人{姓名,状态})很难用面向过程的思维方式进行描述。 * * 面向对象编程 * 这种编程就是抛开计算机思维,使用生活中的思维进行编程的编程方式。面向过程的思维就是描述一个个“动作”, * 而面向对象的思维就是描述一个个“物件”,客观生活中的物件,在程序中我们管“物件”叫做“对象”, * 对象由两部分组成:“属性”和“行为”,对应客观世界中物件的“状态”和“动作”。 */ //面向过程 (function () { //定义电话本 var phonebook = [ {name: 'adang', tel: '1111'}, {name: 'king', tel: '2222'} ]; //查询电话 function getTel(oPhoneBook, oName) { var tel = ''; for (var i = 0; i < oPhoneBook.length; i++) { if (oPhoneBook[i].name === oName) { tel = oPhoneBook[i].tel; break; } } return tel; } //添加记录 function addItem(oPhonebook, oName, oTel) { oPhonebook.push({name: oName, tel: oTel}); } //删除记录 function removeItem(oPhonebook, oName) { var index; for (var i = 0; i < oPhonebook.length; i++) { if (oPhonebook[i].name === oName) { index = i; break; } } if (index !== undefined) { oPhonebook.splice(index, 1); } } //-----test----- //调用函数-增 addItem(phonebook, 'xiaoxiao', '3333'); //调用函数-删 removeItem(phonebook, 'xiaoxiao'); //调用函数-查 getTel(phonebook, 'king'); })(); //面向对象 (function () { //构造函数专注的是对象所具有的属性 function PhonebookManager(oPhonebook) { this._phonebook = oPhonebook; } //所有方法均在构造函数的原型中添加 PhonebookManager.prototype = { addItem: function (oName, oTel) { this._phonebook.push({name: oName, tel: oTel}); }, removeItem: function (oName) { var index, phonebook = this._phonebook; for (var i = 0, len = phonebook.length; i < len; i++) { if (phonebook[i].name === oName) { index = i; break; } } if (index !== undefined) { phonebook.splice(index, 1); } }, getTel: function (oName) { var tel = '', phonebook = this._phonebook; for (var i = 0; i < this._phonebook.length; i++) { if (phonebook.name === oName) { tel = phonebook.tel; break; } } return tel; } } //------test----- var myPhoneManager = new PhonebookManager([ {name: 'adang', tel: '1111'}, {name: 'king', tel: '2222'} ]); //调用函数-增 myPhoneManager.addItem('xiaoxiao', '3333'); //调用函数-删 myPhoneManager.removeItem('xiaoxiao'); //调用函数-查 myPhoneManager.getTel('king'); })();