概念 基于原型的继承 prototype 属性与原型 例 原型链 Object.create(null) & .bind(null) prototype 属性 改变 prototype 内置构造器的 prototype 属性 创建对象-new/原型链 instanceof 实现继承的方式 模拟重载 调用子类方法 链式调用 抽象类 模块化 本文为慕课网 JavaScript深入浅出 JavaScript 面向对象笔记。 概念 面向对象程序设计(Object-oriented programming,OOP)是一种程序设计范型,同时也是一种程序开发的方法。对象指的是类的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性。 ——维基百科 一般面向对象包含:继承,封装,多态,抽象 基于原型的继承 function Foo() { this.y = 2; } console.log(typeof Foo.prototype); //object Foo.prototype.x = 1; var obj3 = new Foo(); console.log(obj3.y); //2 console.log(obj3.x); //1 创建函数 Foo 的时候,就会有一个内置的 Foo.prototype 属性,并且这个属性是对象。 在使用 new Foo(); 创建对象实例时。this 会指向一个对象,并且这个对象的原型会指向 Foo.prototype 属性。this.y = 2 给这个对象赋值,并把这个对象返回。把这个对象赋值给 obj3。 y 是 obj3 上的,x 是 obj3 的原型 Foo.prototype 上的。 prototype 属性与原型 prototype 是函数对象上预设的对象属性。 原型是对象上的原型,通常是构造器的 prototype 属性。 例 function Person(name, age) { this.name = name; this.age = age; } Person.prototype.LEGS_NUM = 2; Person.prototype.ARMS_NUM = 2; Person.prototype.hi = function() { console.log('Hi, my name is ' + this.name + ". I'm " + this.age + ' years old now'); }; Person.prototype.walking = function() { console.log(this.name + ' is walking...'); }; function Student(name, age, className) { Person.call(this, name, age); //使 Person 中的 this 指向 Student this.className = className; } Student.prototype = Object.create(Person.prototype); Student.prototype.constructor = Student; Student.prototype.hi = function() { console.log('Hi, my name is ' + this.name + ". I'm " + this.age + ' years old now, and from ' + this.className + "."); }; Student.prototype.learn = function(subject) { console.log(this.name + ' is learning ' + subject + ' at ' + this.className + '.'); } //test var gao = new Student('Gao', '24', 'Class 3123'); console.log(gao); // 这个对象的具体内容见下图 gao.hi(); //Hi, my name is Gao. I'm 24 years old now, and from Class 3123. gao.LEGS_NUM; //2 gao.walking(); //Gao is walking... gao.learn('JavaScript'); //Gao is learning JavaScript at Class 3123. Object.create(arg) 创建一个空对象,并且这个对象的原型指向参数 arg。 Student.prototype.constructor = Student 为了保证一致性,否则 constructor 指向 Person。 原型链 gao 对象的原型链: 下面通过图形展示原型链: Object.create(null) & .bind(null) 这两种算是特例。 Object.create(null) 和 .bind(null) 这两种方式创建出来的对象是没有 prototype 属性的,为 undefined。 prototype 属性 改变 prototype JavaScript 中的 prototype 是对象,在运行的时候可以修改。 给 prototype 添加或删除一些属性,是会影响到已经创建好的实例对象的。 但是,直接修改 prototype 属性,是不会影响到已经创建好的实例对象的。但是会影响到新的实例对象。如下代码: // 上接上面的代码 // 给 prototype 添加或删除一些属性 Student.prototype.x = 101; console.log(gao.x); //101 // 直接修改 prototype 属性 Student.prototype = { y: 2 }; // 不会影响到已创建好的实例对象 console.log(gao.x); //101 console.log(gao.y); //undefined // 会影响到新创建的实例对象 var ying = new Student('Ying', 24, 'UI'); console.log(ying.x); //undefined console.log(ying.y); //2 内置构造器的 prototype 属性 修改内置构造器的 prototype 属性后,在实例化这个对象后,枚举其属性时,会把修改的内置构造器的 prototype 属性也枚举出来,有时候这是要避免的。可用 defineProperty 方法解决。如下代码: Object.prototype.x = 1; var obj = {}; console.log(obj.x); //1 console.log(obj); for (var k in obj) { console.log('result--->' + k); } // result--->x 使用 defineProperty 后: Object.defineProperty(Object.prototype, 'x', { writable: true, value: 1 }); var obj = {}; console.log(obj.x);//1 console.log(obj); for (var k in obj) { console.log('result--->' + k); } // nothing output here 其实也可以这样枚举,使用 hasOwnProperty 方法: for (var key in obj) { if (obj.hasOwnProperty(key)) { console.log("result--->" + key); } } 创建对象-new/原型链 instanceof console.log([1, 2] instanceof Array); //true console.log([1, 2] instanceof Object); //true console.log(new Object() instanceof Array); //false 左边要求是对象,右边要求是构造器或函数。它会判断:右边的构造器中的 prototype 属性是否出现在左边的对象的原型链上。 注意:不同的 window 或 iframe 间的对象类型检测不能使用 instanceof! 实现继承的方式 function Person() {} function Student() {} Student.prototype = Person.prototype; //1 Student.prototype = new Person(); //2 Student.prototype = Object.create(Person.prototype); //3 Student.prototype.constructor = Student; 注释中: 1 是错误的。如果改变了 Student 就会改变 Person 2 可以实现继承,但是其调用了构造函数,若父类构造函数中有形参,那么传值就会比较奇怪。 3 是最好的方法。创建了一个空对象,并且对象的原型指向参数 Person.prototype。这样便实现了继承。同时原型链写,不向上查找。但是 Object.create 是ES5 中的方法,所以可以使用下列代码做兼容: if (!Object.create) { Object.create = function(proto) { function F() {} F.prototype = proto; return new F; }; } 模拟重载 function Person() { var args = arguments; if (typeof args[0] === 'object' && args[0]) { if (args[0].name) { this.name = args[0].name; } if(args[0].age){ this.age = args[0].age; } } else { if (args[0]) { this.name = args[0]; } if (args[1]) { this.age = args[1]; } } } //重写 toString 方法 Person.prototype.toString = function() { console.log('name='+this.name+', age='+this.age); }; var gao = new Person({name:'Gao',age:24}); gao.toString(); // name=Gao, age=24 var ying = new Person('Ying',25); ying.toString(); // name=Ying, age=25 对参数进行判断,模拟实现重载。 调用子类方法 function Person(name) { this.name = name; } function Student(name, className) { this.className = className; Person.call(this, name); // 调用基类的构造器 } var gao = new Student('Gao', '3123'); console.log(gao); // Student {className: "3123", name: "Gao"} Person.prototype.init = function() {}; Student.prototype.init = function() { // do sth... Person.prototype.init.apply(this, arguments); // 同时也想调用父类被覆盖的方法 }; 主要是两种:调用父类的构造器,调用原型链上父类被覆盖的方法。 链式调用 function ClassManager() {} ClassManager.prototype.addClass = function(str) { console.log('Class: ' + str + ' added'); return this; }; var manager = new ClassManager(); manager.addClass('classA').addClass('classB').addClass('classC'); // Class: classA added // Class: classB added // Class: classC added 重点在于 return this。返回这个 ClassManager 的实例。这样这个实例又可以继续调用方法。 抽象类 在构造器中 throw new Error(''); 抛异常。这样防止这个类被直接调用。 function DetectorBase() { throw new Error('Abstract class can not be invoked directly!'); } DetectorBase.detect = function() { console.log('Detection starting...'); } DetectorBase.stop = function() { console.log('Detection stopped.'); }; DetectorBase.init = function() { throw new Error('Error'); } var d = new DetectorBase();// Uncaught Error: Abstract class can not be invoked directly! function LinkDetector() {} LinkDetector.prototype = Object.create(DetectorBase.prototype); LinkDetector.prototype.constructor = LinkDetector; var l = new LinkDetector(); console.log(l); //LinkDetector {}__proto__: LinkDetector l.detect(); //Uncaught TypeError: l.detect is not a function l.init(); //Uncaught TypeError: l.init is not a function var d = new DetectorBase(); 是不能实例化的,会报错 l.detect(); 但是这个为什么报错我就不知道了。 已经在原课程下提问了,期待老师的讲解。 抽象类中子类为什么不能调用父类的非抽象方法? 问题已经解决了,应该是老师当时的课件写错了,应该再基类中将这两个方法写在其原型 prototype 上。如下: function DetectorBase() { throw new Error('Abstract class can not be invoked directly!'); } DetectorBase.prototype.detect = function() { console.log('Detection starting...'); }; DetectorBase.prototype.stop = function() { console.log('Detection stopped.'); }; DetectorBase.prototype.init = function() { throw new Error('Error'); }; // var d = new DetectorBase();// Uncaught Error: Abstract class can not be invoked directly! function LinkDetector() {} LinkDetector.prototype = Object.create(DetectorBase.prototype); LinkDetector.prototype.constructor = LinkDetector; var l = new LinkDetector(); console.log(l); //LinkDetector {}__proto__: LinkDetector l.detect(); //Detection starting... l.init(); //Uncaught Error: Error 模块化 var moduleA; moduleA = function() { var prop = 1; function func() {} return { func: func, prop: prop }; }(); // 立即执行匿名函数 prop,func 不会被泄露到全局作用域。 或者另一种写法,使用 new moduleA = new function() { var prop = 1; function func() {} this.func = func; this.prop = prop; } 更复杂的可以使用 Sea.js Kissy Require.js 模块化工具。 最后补充一点设计模式相关的资料,我还没有来得及看的: 学用 JavaScript 设计模式 常用的Javascript设计模式 JavaScript设计模式深入分析