1、原型对象概述
- 构造函数的缺点
- 通过构造函数为实例对象定义属性,虽然很方便,但是有一个缺点。同一个构造函数的多个实例之间,无法共享属性,从而造成对系统资源的浪费。
- prototype属性的作用
- JavaScript 继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享,不仅节省了内存,还体现了实例对象之间的联系。
- JavaScript 规定,每个函数都有一个prototype属性,指向一个对象。
- 对于普通函数来说,该属性基本无用。但是,对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。
原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现在所有实例对象上。
如果实例对象自身就有某个属性或方法,它就不会再去原型对象寻找这个属性或方法。
总结一下,原型对象的作用,就是定义所有实例对象共享的属性和方法。
- 什么是原型链
- JavaScript 规定,所有对象都有自己的原型对象(prototype)。一方面,任何一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”(prototype chain):对象到原型,再到原型的原型……
- 如果一层层地上溯,所有对象的原型最终都可以上溯到Object.prototype,即Object构造函数的prototype属性。
- Object.prototype的原型是null。null没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是null。
- 原型对象的constructor属性。
- prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。
- constructor属性表示原型对象与构造函数之间的关联关系,如果修改了原型对象,一般会同时修改constructor属性,防止引用的时候出错。
function Person(name) { this.name = name; } Person.prototype.constructor === Person // true Person.prototype = { method: function () {} }; Person.prototype.constructor === Person // false Person.prototype.constructor === Object // true
2、instanceof运算符的作用和原理
- instanceof运算符返回一个布尔值,表示对象是否为某个构造函数的实例。
- instanceof的原理是检查右边构造函数的prototype属性,是否在左边对象的原型链上。有一种特殊情况,就是左边对象的原型链上,只有null对象。这时,instanceof判断会失真。
- 整体继承
- 第一步是在子类的构造函数中,调用父类的构造函数。
function Sub(value) { Super.call(this); this.prop = value; } // 上面代码中,Sub是子类的构造函数,this是子类的实例。在子类实例上调用父类的构造函数Super,就会让子类实例具有父类实例的属性。
2. 第二步,是让子类的原型指向父类的原型,这样子类就可以继承父类原型。
Sub.prototype = Object.create(Super.prototype); Sub.prototype.constructor = Sub; Sub.prototype.method = ‘...‘; // 举例来说,下面是一个Shape构造函数。 function Shape() { this.x = 0; this.y = 0; } Shape.prototype.move = function (x, y) { this.x += x; this.y += y; console.info(‘Shape moved.‘); }; // 我们需要让Rectangle构造函数继承Shape。 // 第一步,子类继承父类的实例 function Rectangle() { Shape.call(this); // 调用父类构造函数 } // 另一种写法 function Rectangle() { this.base = Shape; this.base(); } // 第二步,子类继承父类的原型 Rectangle.prototype = Object.create(Shape.prototype); Rectangle.prototype.constructor = Rectangle;
- 单个方法继承
// 有时只需要单个方法的继承,这时可以采用下面的写法。 ClassB.prototype.print = function() { ClassA.prototype.print.call(this); // some code } // 上面代码中,子类B的print方法先调用父类A的print方法,再部署自己的代码。这就等于继承了父类A的print方法。
4、怎么用构造函数实现一个多重继承
- JavaScript 不提供多重继承功能,即不允许一个对象同时继承多个对象。但是,可以通过变通方法,实现这个功能。
function M1() { this.hello = ‘hello‘; } function M2() { this.world = ‘world‘; } function S() { M1.call(this); M2.call(this); } // 继承 M1 S.prototype = Object.create(M1.prototype); // 继承链上加入 M2 Object.assign(S.prototype, M2.prototype); // 指定构造函数 S.prototype.constructor = S; var s = new S(); s.hello // ‘hello‘ s.world // ‘world‘ // 上面代码中,子类S同时继承了父类M1和M2。这种模式又称为 Mixin(混入)。
- 什么是模块
- 模块是实现特定功能的一组属性和方法的封装。
- JavaScript 不是一种模块化编程语言,ES6 才开始支持“类”和“模块”。下面介绍传统的做法,如何利用对象实现模块的效果。
- 基本实现方法及缺点
- 简单的做法是把模块写成一个对象,所有的模块成员都放到这个对象里面。
- 这样的写法会暴露所有模块成员,内部状态可以被外部改写。
var module1 = new Object({ _count : 0, m1 : function (){ //... }, m2 : function (){ //... } }); module1.m1(); module1._count = 5;
- 用构造函数封装私有变量及缺点
- 我们可以利用构造函数,封装私有变量。
- 这种方法将私有变量封装在构造函数中,导致构造函数与实例对象是一体的,总是存在于内存之中,无法在使用完成后清除。这意味着,构造函数有双重作用,既用来塑造实例对象,又用来保存实例对象的数据,违背了构造函数与实例对象在数据上相分离的原则。
function StringBuilder() { var buffer = []; this.add = function (str) { buffer.push(str); }; this.toString = function () { return buffer.join(‘‘); }; }
- IIFE方式封装私有变量,JavaScript模块的基本写法
// 使用“立即执行函数”(Immediately-Invoked Function Expression,IIFE),将相关的属性和方法封装在一个函数作用域里面,可以达到不暴露私有成员的目的。 var module1 = (function () { var _count = 0; var m1 = function () { //... }; var m2 = function () { //... }; return { m1 : m1, m2 : m2 }; })(); // 使用上面的写法,外部代码无法读取内部的_count变量。 console.info(module1._count); //undefined // 上面的module1就是 JavaScript 模块的基本写法。
- 模块的放大模式和宽放大模式
-
- 放大模式
var module1 = (function (mod){ mod.m3 = function () { //... }; return mod; })(module1); // 上面的代码为module1模块添加了一个新方法m3(),然后返回新的module1模块。
-
- 宽放大模式
var module1 = (function (mod) { //... return mod; })(window.module1 || {}); // 与"放大模式"相比,“宽放大模式”就是“立即执行函数”的参数可以是空对象。
2. 输入全局变量
-
- 独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。
- 为了在模块内部调用全局变量,必须显式地将其他变量输入模块。
var module1 = (function ($, YAHOO) { //... })(jQuery, YAHOO); // 上面的module1模块需要使用 jQuery 库和 YUI 库,就把这两个库(其实是两个模块)当作参数输入module1。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。
3. 命名空间的作用
(function($, window, document) { function go(num) { } function handleEvents() { } function initialize() { } function dieCarouselDie() { } //attach to the global scope window.finalCarousel = { init : initialize, destroy : dieCarouselDie } })( jQuery, window, document ); // 上面代码中,finalCarousel对象输出到全局,对外暴露init和destroy接口,内部方法go、handleEvents、initialize、dieCarouselDie都是外部无法调用的。