灏天阁

构造函数原型继承

· Yin灏

构造函数

定义构造函数

再语法和用法上,构造函数和普通函数没有任何区别。

function 类型名称 (配置参数) {
    this.属性1 = 属性值1;
    this.属性2 = 属性值2;
    ...
}

构造函数显著特点

  • 函数内使用 this,引用将要生成的实例对象。
  • 必须使用 new 调用函数,生成实例对象。

定义构造函数,包含两个属性一个方法:

function Point(x, y) {
    this.x = x;
    this.y = y;
    this.sum = function() {
        return this.x + this.y;
    }
}

调用构造函数

使用 new 可以调用构造函数,创建实例,并返回这个对象。

function Point(x, y) {
    this.x = x;
    this.y = y;
    this.sum = function() {
        return this.x + this.y;
    }
}
var p1 = new Point(100, 200);
var p2 = new Point(300, 400);
console.log(p1.x);
console.log(p2.x);
console.log(p1.sum());
console.log(p2.sum());
/*
  构造函数可以接收参数,以便初始化实例对象,如果不需要传参,可以省略小括号,直接使用 new;下面的代码是等价的。
  var p1 = new Point()
  var p2 = new Point
*/

如果不使用 new ,直接使用小括号的构造函数,就是普通函数,不会生成实例对象,this 就代表调用函数的对象,再客户端指代的全局对象 window

为了避免这个错误,最有效的办法就是使用严格模式。

function Point(x, y) {
    "use strict";
    this.x = x;
    this.y = y;
    this.sum = function() {
        return this.x + this.y;
    }
}
/*
  这样调用构造函数,必须使用 new,否则报错。
  或者使用 if 对 this 进行检测,如果 this 不是实例对象,就返回实例对象;如下:
*/
function Point(x, y) {
    if(!(this instanceof Point)) return new Point(x, y);
    this.x = x;
    this.y = y;
    this.sum = function() {
        return this.x + this.y;
    }
}

构造函数的返回值

构造函数允许使用 return 语句,如果返回的值为简单值,则将被忽略,直接返回 this 指代的实例对象;如果返回值为对象,则将覆盖 this 指代的实例,返回 return 后面跟随的对象。

下面示例在构造函数内部定义 return 返回一个对象直接量,当使用 new 命令调用构造函数时,返回的不是 this 指代的实例,而是这个对象直接量。

function Point(x ,y) {
  this.x = x;
  this.y = y;
  return {x: true, y:false}
}

var p1 = new Point(100, 200);
console.log(p1); // {x: true, y: false}

引用构造函数

在普通函数内,使用 arguments.callee 可以引用函数自身。如果在严格模式下,是不允许使用 arguments.callee 引用函数,这时可以使用 new.target 来访问函数。

new.target 可以在构造函数中使用,但是普通函数不行。

function Point(x ,y) {
  "use strict";
  // 检测 this 是否为实例对象
  if(!(this instanceof new.target)) return new new.target(x, y);
  this.x = x;
  this.y = y;
}

var p1 = new Point(100, 200);
console.log(p1); // Point {x: 100, y: 200}

this 指针

使用 this 指针

this 是由 JS 在执行函数时自动生成的,存在于函数内的一个动态指针,指代当前调用对象。

下面使用 call 方法不断改变函数内部 this 的指代。

var x = "window";

function a() {
  this.x = "a";
}

function b() {
  this.x = "b";
}

function c() {
  console.log(x)
}

function f() {
  console.log(this.x)
}

f();  // "window"

f.call(window); // "window"

f.call(new a()); // "a" this 指向函数 a() 的实例

f.call(new b()); // "b" this 指向函数 b() 的实例

f.call(c); // undefined this 指向函数 c 对象

下面总结 this5 种常用场景中的表现以及应对策略

  • 普通调用

下面演示函数引用和函数调用对 this 的影响。

var obj = { // 父对象
  name: '父对象obj',
  func: function() {
    return this;
  }
}

obj.sub_obj = { // 子对象
  name: '子对象 sub_obj',
  func: obj.func // 引用父对象 obj 的方法 func
}

var who = obj.sub_obj.func();
console.log(who); // {name: "子对象 sub_obj", func: ƒ}
// 返回子对象 sub_obj ,说明 this 代表 sub_obj

如果把子对象 sub_objfunc 改为函数调用

var obj = { // 父对象
  name: '父对象obj',
  func: function() {
    return this;
  }
}

obj.sub_obj = { // 子对象
  name: '子对象 sub_obj',
  func: obj.func() // 调用父对象 obj 的方法 func
}

var who = obj.sub_obj.func;
console.log(who); // {name: "父对象obj", sub_obj: {…}, func: ƒ}

则函数中的 this 所代表的时定义函数时所在的父对象 obj

  • 实例化

使用 new 调用函数时,this 总是指代实例对象。

var obj = {};

obj.func = function() {
  if(this == obj) console.log("this = obj");
  else if(this == window) console.log("this = window");
  else if(this.constructor == arguments.callee) console.log("this = 实例对象")
}

new obj.func; //实例化 this = 实例对象
  • 动态调用

使用 callapply 可以强制改变 this,使其指向参数对象。

function func() {
  // 如果 this 的构造函数等于当前函数,说明 this 为实例对象
  if(this.constructor == arguments.callee) console.log("this = 实例对象");
  // 如果 this 等于 window,则表示 this 为 window 对象
  else if(this == window) console.log("this = window 对象");
  // 如果 this 为其他对象,则表示 this 为其他对象
  else console.log("this == 其他对象\n this.constructor = " + this.constructor);
}

func(); // this = window 对象
new func(); // this = 实例对象

// 由于 call() 方法的参数值为数字 1,则 JS 会把数字 1 强制封装为数值对象,此时 this 就会指向这个数值对象。
func.call(1); // this 指向数值对象
// this == 其他对象 this.constructor = function Number() { [native code] }
  • 事件处理

在事件处理函数中,this 总是指向触发该事件的对象。

var button = document.getElementsByTagName("input")[0];
var obj = {};
obj.func = function() {
  if(this == obj) console.log("this = obj");
  if(this == window) console.log("this = window");
  if(this == button) console.log("this = button")
}

button.onclick = obj.func; // this = button
/*
  this 指向 button,因为 func() 是被传递给按钮的事件处理函数之后才被调用执行的。
*/
  • 定时器

使用定时器函数。

var obj = {};
obj.func = function() {
  if(this == obj) console.log("this = obj");
  else if(this == window) console.log("this = window");
  else if(this.constructor == arguments.callee) console.log("this = 实例对象");
  else console.log("this == 其他对象\n this.constructor = " + this.constructor);
}

setTimeout(obj.func, 100); // window

在符合 DOM 标准的浏览器中,this 指向 window 对象,而不是 button 对象。

因为 setTimeout 在全局的作用域中执行,所以 this 指向 window 对象。要解决浏览器兼容性问题,可以使用 callapply方法来实现。

var obj = {};
obj.func = function() {
  if(this == obj) console.log("this = obj");
  else if(this == window) console.log("this = window");
  else if(this.constructor == arguments.callee) console.log("this = 实例对象");
  else console.log("this == 其他对象\n this.constructor = " + this.constructor);
}

setTimeout(function() {
  obj.func.call(obj); // this = obj
}, 100);

this 安全策略

由于 this 的不确定性,会给开发带来很多风险,因此使用 this 时,应该时刻保持谨慎。

锁定 this 有以下两种基本方法:

  1. 使用私有变量存储 this
  2. 使用 callapply 强制固定 this 的值

使用 this 作为参数来调用函数,可以避免产生 this 因环境变化为变化的问题

<input type="button" value="按钮" onclick="func(this)">
let func = (_this) => {
    console.log(_this.value); // 按钮
}

使用私有变量存储 this,设置静态指针

/*
  在构造函数中把 this 存储在私有变量中,然后在方法中使用私有变量来引用构造函数的 this,这样在类型实例化后,方法内的 this 不会发生变化。
*/
function Base() {
    var _this = this;
    this.func = function() {
        return _this;
    }
    this.name = "Base";
}

function Sub() {
    this.name = "Sub";
}

Sub.prototype = new Base();
var sub = new Sub();

var _this = sub.func();
// this 始终指向基类实例,而不是子类实例
console.log(_this); // Object { func: func(), name: "Base" }

使用 callapply 强制固定 this 的值

// 使用 call() 或 apply() 方法强制指定 this 的指代对象
// 把 this 转换为静态指针
// 参数 obj 表示预设值 this 所指代的对象,返回一个预备调用的函数
Function.prototype.pointTo = function(obj) {
    var _this = this; // 存储当前函数对象
    return function() { // 返回一个闭包函数
        return _this.apply(obj, arguments); // 返回执行当前函数,并强制设置为指定对象
    }
}
// 把 this 转换为静态指针
// 参数 obj 表示预设值 this 所指代的对象,返回一个预备调用的函数
Function.prototype.pointTo = function(obj) {
    var _this = this; // 存储当前函数对象
    return function() { // 返回一个闭包函数
        return _this.apply(obj, arguments); // 返回执行当前函数,并强制设置为指定对象
    }
}

var obj1 = {
    name: "this = obj"
}

obj1.func = (function(){
    return this;
}).pointTo(obj1); // 把 this 绑定到对象 obj1 身上

var obj2 = {
    name: 'this = obj2',
    func: obj1.func
}

var _this = obj2.func();
console.log(_this.name); // this = obj

绑定函数

绑定函数是为了纠正函数的执行上下文,把 this 绑定到指定对象上,避免在不同执行上下文中调用函数时, this 指代的对象不断变化。

function bind(fn, context) { // 绑定函数
    return function() {
        return fn.apply(context, arguments); // 在指定上下文对象上动态调用函数
    }
} 
/*
  bind() 函数接收一个函数和一个上下文环境,返回一个在给定环境中调用给定函数的函数,并且将返回函数的所有的参数原封不动的传递给调用函数。
*/
var handler = {
    message: 'handler',
    click: function(event) {
        console.log(this.message);
    }
}

var btn = document.getElementById('btn');
btn.addEventListener('click', handler.click); // undefined
/*
  测试发现,this 最后指向的 DOM 按钮,而不是 handler
  下面使用闭包进行修正
*/
var handler = {
    message: 'handler',
    click: function(event) {
        console.log(this.message);
    }
}

var btn = document.getElementById('btn');
btn.addEventListener('click', function() {
    handler.click(); // handler
});
/*
  方法改进
*/
function bind(fn, context) {
    return function() {
        return fn.apply(context, arguments);
    }
} 

var handler = {
    message: 'handler',
    click: function(event) {
        console.log(this.message);
    }
}

var btn = document.getElementById('btn');
// 改变了执行的上下文
btn.addEventListener('click', bind(handler.click, handler));

使用 bind

用来把函数绑定到指定对象上。在绑定函数中,this 对象被解析为传入的对象。

var check = function(value) {
    if(typeof value !== 'number') return false;
    else return value >= this.min && value <= this.max;
}

var range = {min:10, max:20};
var check1 = check.bind(range);
var result = check1(12);
console.log(result); // true
var obj = {
    min: 50,
    max: 100,
    check: function(value) {
        if(typeof value !== 'number') {
            return false;
        } else {
            return value >= this.min && value <= this.max;
        }
    }
}

var result = obj.check(10);
console.log(result); // false

var range = {min: 10, max:20};

var check1 = obj.check.bind(range);

console.log(check1(10)); // true
// 演示如何利用 bind() 方法为函数传递两次参数值,以便实现连续参数求值计算
var func = function(val1, val2, val3, val4) {
    console.log(val1 + " " + val2 + " " + val3 + " " + val4);
}

var obj = {};

var func1 = func.bind(obj, 12, 'a');

func1("b", "c"); // 12 a b c

链式语法

实现方法:设计每一个方法的返回值都是 jQuery 对象(this) 。

Function.prototype.method = function(name, func) {
    if(!this.prototype[name]) {
       this.prototype[name] = func;
       return this;
    }
}

String.method('trim', function() {
    return this.replace(/^\s+|\s+$/g, '');
})

String.method('writeln', function() {
    console.log(this);
    return this;
})

String.method('log', function() {
    console.log(this);
    return this;
})

var str = "abc";
str.trim().writeln().log();

原型

函数都由原型,函数实例化后,实例对象通过 prototype 可以访问原型,实现继承机制。

定义原型

原型实际上就是一个普通对象,继承于 Object 类,由 JS 自动创建并依附于每个函数身上。

function P(x) {
    this.x = x;
}

console.log(P);

P.prototype.x = 1;

console.log(P);

var p1 = new P(10);
console.log(p1); // {x: 10}

P.prototype.x = p1.x; // 修改原型属性 x 的值
console.log(P.prototype.x); // 10

访问原型

访问原型对象的 3 种方式:

obj.__proto__

obj.constructor.prototype

Object.getPrototypeOf(obj)

var F = function() {};
var obj = new F();

var proto1 = Object.getPrototypeOf(obj);
var proto2 = obj.__proto__;
var proto3 = obj.constructor.prototype;
var proto4 = F.prototype;

设置原型

设置原型的 3 种方法:

/*
  obj.__proto__ = prototypeObj
  Object.setPrototypeOf(obj, prototypeObj);
  Object.create(prototypeObj);
*/
var proto = {
    name: "prototype"
}

var obj1 = {}
obj1.__proto__ = proto;
console.log(obj1);

var obj2 = {};
Object.setPrototypeOf(obj2, proto);
console.log(obj2);

var obj3 = Object.create(proto);
console.log(obj3.name); // 'prototype'

检测原型

使用 isPrototypeOf() 可以判断对象是否为参数对象的原型。

var F = function() {}
var obj = new F();

var proto1 = Object.getPrototypeOf(obj);
console.log(proto1);
console.log(proto1.isPrototypeOf(obj)); // true
// 也可以使用下面的方式,检测不同类型的实例
var proto = Object.prototype;
console.log(proto.isPrototypeOf({})); // true
console.log(proto.isPrototypeOf([])); // true
console.log(proto.isPrototypeOf(function(){})); // true
console.log(proto.isPrototypeOf(null)); // false

原型属性和私有属性

原型属性可以被所有实例访问,而私有属性只能被当前实例访问。

function f() {
    this.a = 1;
    this.b = function() {
        return this.a;
    }
}

var e = new f();
console.log(e.a); // 1
console.log(e.b()); // 1
// 私有属性可以在实例中被修改,不同实例之间不会相互干扰
function f() {
    this.a = 1;
}

var e = new f();
var g = new f();

console.log(e.a); // 1
console.log(g.a); // 1

e.a = 2;

console.log(e.a); // 2
console.log(g.a);
// 原型属性将会影响所有实例对象,修改任何原型属性值,则该构造函数的所有实例都会看到这种变化。
function f() {}
f.prototype.a = 1;

var e = new f();
var g = new f();

console.log(e.a); // 1
console.log(g.a); // 1

f.prototype.a = 2;
console.log(e.a); // 2
console.log(g.a); // 2
function p(x, y, z) {
    this.x = x;
    this.y = y;
    this.z = z;
}

p.prototype.del = function() {
    for(var i in this) {
        delete this[i];
    }
}

p.prototype = new p(1,2,3);

var p1 = new p(10, 20, 30);

console.log(p1.x); // 10
console.log(p1.y); // 20
console.log(p1.z); // 30

p1.del(); // 删除所有的私有属性

console.log(p1.x); // 1
console.log(p1.y); // 2
console.log(p1.z); // 3

应用原型

// 利用原型属性为对象设置默认值
function p(x) {
    if(x) {
        this.x = x;
    }
}

p.prototype.x = 0;
var p1 = new p();
console.log(p1.x); //0

var p2 = new p(1);
console.log(p2.x); // 2
// 利用原型间接实现本地数据备份。把本地对象的数据完全赋值给原型对象,相当于为该对象顶一个副本,也就是备份对象。
// 当对象属性改变的时候,就可以通过原型对象来恢复本地对象的初始值
function p(x) {
    this.x = x;
}

p.prototype.backup = function() {
    for(var i in this) {
        p.prototype[i] = this[i];
    }
}

var p1 = new p(1);
p1.backup();
p1.x = 10;

console.log(p1.x); // 10
p1 = p.prototype;
console.log(p1.x); // 1
// 利用原型为对象属性设置 “只读” 属性
function p(x, y) { // 求坐标点构造函数
    if(x) this.x = x; // 初始 x 轴值
    if(y) this.y = y; // 初始 y 轴值
    p.prototype.x = 0; // 默认 x 轴值
    p.prototype.y = 0; // 默认 y 轴值
}

function l(a, b) { // 求两点距离的构造函数
    var a = a; // 参数私有化
    var b = b; // 参数私有化
    var w = function() { // 计算 x 轴距离
        return Math.abs(a.x - b.x);
    }
    var h = function() { // 计算 y 轴距离
        return Math.abs(a.y - b.y);
    }
    this.length = function() {
        return Math.sqrt(w() * w() + h() * h());
    }
    this.b = function() {
        return a; // 获取起点坐标对象
    }
    this.e = function() {
        return b; // 获取终点坐标对象
    }
}

var p1 = new p(1, 2); // 声明一个点
var p2 = new p(10, 20); // 声明另一个点
console.log(p1); // {x:1, y:2}
console.log(p2); // {x:10, y:20}

var l1 = new l(p1, p2);
console.log(l1.length()); // 20.12461179749811

l1.b().x = 50;

console.log(l1.length()); // 43.86342439892262
/*

  通过 b() 和 e() 可以随意的更改坐标值。
  为了避免因为改动方法 b() 的属性 x 值会影响两点距离,可以在方法 b() 和 e() 中新建一个临时性的构造类,设置该类的原型为 a,然后实例化构造类并返回,这样就阻断了方法 b() 与 私有变量 a 的直接联系。

*/
this.b = function() {
    function temp() {};
    temp.prototype = a;
    return new temp();
}
this.e = function() {
    function temp(){};
    temp.prototype = a;
    return new temp();
}
/*
  还有一种方法是在给私有变量 w 和 h 赋值时,不是赋值函数,而是函数调用表达式,这样私有变量 w 和 h 存储的是值类型数据,而不是对函数结构的引用,从而就不再受后期相关属性值的影响。
*/
var w = function() { // 计算 x 轴距离
    return Math.abs(a.x - b.x);
}();
var h = function() { // 计算 y 轴距离
    return Math.abs(a.y - b.y);
}();
// 利用原型进行批量复制
function f(x) {
    this.x = x;
}

var a = [];

function temp() {}

temp.prototype = new f(10);

for(var i = 0; i < 100; i++) {
    a[i] = new temp();
}

console.log(a)

原型链

function a(x) {
  this.x = x;
}

a.prototype.x = 0;

function b(x) {
  this.x = x;
}

b.prototype = new a(1);

function c(x) {
  this.x = x;
}

c.prototype = new b(2);

var d = new c(3);

console.log(d.x); // 3

delete d.x;

console.log(d.x); // 2

delete c.prototype.x;

console.log(d.x); // 1

delete b.prototype.x;

console.log(d.x); // 0

delete a.prototype.x;

console.log(d.x); // undefined
/*
  在 js 中,一切皆对象,函数是第一型。Function 和 Object 都是函数的实例。构造函数的父原型指向 Function 的原型,Function.prototype 的原型是 Object 的原型,Object 的原型也指向 Function 的原型,Object.prototype 是所有原型的顶层
*/
Function.prototype.a = function() {
  console.log("Function")
}

Object.prototype.a = function() {
  console.log('Object');
}

function f() {
  this.a = "a";
}

f.prototype = {
  w: function() {
    console.log("w");
  }
}

// f 是 Function 的实例
console.log(f instanceof Function); // true
// f 的原型也是对象
console.log(f.prototype instanceof Object); // true
// Function 也是 Object 的实例
console.log(Function instanceof Object); // true
// Function 原型是 Object 的实例
console.log(Function.prototype instanceof Object); // true
// Object 是 Function 的实例
console.log(Object instanceof Function); // true
// Object.prototype 是原型顶层
console.log(Object.prototype instanceof Function); // false

原型继承

使用原型继承的方法设计类继承。

function A(x) {
  this.x1 = x;
  this.get1 = function() {
    return this.x1;
  }
}

function B(x) {
  this.x2 = x;
  this.get2 = function() {
    return this.x2 + this.x2;
  }
}

B.prototype = new A(1);

function C(x) {
  this.x3 = x;
  this.get3 = function() {
    return this.x3 + this.x3;
  }
}

C.prototype = new B(2);

扩展原型方法

通过 prototype 为原生类型扩展方法,扩展方法可以被所有对象调用。

Function.prototype.method = function(name, func) {
  if(!this.prototype[name]) {
      this.prototype[name] = func;
      return this;
  }
}

Number.method('int', function() {
  return Math[this < 0 ? 'ceil' : 'floor'](this);
})

console.log((-10 / 3).int()); // -3

String.method('trim', function() {
  return this.replace(/^\s+|\s+$/g, '');
})

console.log('"' + " abc ".trim() + '"'); // "abc"

类型

构造原型

直接使用 prototype 原型设计类的继承存在两个问题:

  1. 由于构造函数事先声明,而原型属性在类的结构声明之后才被定义,因此无法通过构造函数参数向原型动态传参。这样实例化对象都是一个模样,没有个性,要改变原型属性值,则所有实例都会受到干扰。
  2. 当原型属性的值为引用类型数据时,如果在一个对象实例中修改该属性值,将会影响所有的实例。
function Book() {} // 声明构造函数

Book.prototype.o = {x:1, y:2};

var book1 = new Book();
var book2 = new Book();

console.log(book1.o.x); // 1
console.log(book2.o.x); // 1

book2.o.x = 3;

console.log(book1.o.x); // 3
console.log(book2.o.x); // 3
/*
  由于原型属性 o 是一个引用类型,所以所有实例的属性 o 的值都是同一个对象的引用,一旦 o 的值发生了变化,将会影响所有实例。
*/

对于可能会相互影响的原型属性,并且希望动态传递参数的属性,可以吧它们独立出来使用构造函数模式进行设计。对于不需要个性设计、具有共性的方法或属性,则可以使用原型模式来设计。

function Book(title, pages) {
  this.title = title;
  this.pages = pages;
}

Book.prototype.what = function() {
  console.log(this.title + this.pages);
}

var book1 = new Book("js程序设计", 160);
var book2 = new Book("c程序设计", 240);

console.log(book1.title); // js程序设计
console.log(book2.title); // c程序设计

book1.what(); // js程序设计160

/*
  一般建议使用构造函数模式定义所有属性,使用原型模式定义所有方法,这样所有方法都只创建一次,而每个实例都能够根据需要设置属性值。这也是最广的一种设计模式。
*/

动态原型

根据面向对象的设计原则,类型的所有成员应该都被封装在类结构体内。

function Book(title, pages) {
    this.title = title;
    this.pages = pages;
    Book.prototype.what = function() {
        console.log(this.title + this.pages)
    }
}

但当每次实例化时,类 Book 中包含的原型方法就会被重复创建,生成大量的原型方法,浪费资源。可以使用 if 判断原型方法是否存在,如果存在就不重复创建。

function Book(title, pages) {
  this.title = title;
  this.pages = pages;
  if(typeof Book.isLock == 'undefined') {
    // 这里使用类名 Book,而没有使用 this,这是因为原型是属于类本身的,而不是对象实例的。
    Book.prototype.what = function() {
      console.log(this.what + this.pages);
    };
    Book.isLock = true;
  }
}

var book1 = new Book('JS 程序设计', 160);
var book2 = new Book('C 程序设计', 240);

console.log(book1.title);
console.log(book2.title);

工厂模式

工厂模式是定义类型的基本方法,也是 JS 最常见的一种开发模式。它把对象实例化简单封装在一个函数中,然后通过调用函数,实现快速、批量生产实例对象。

function Car(color, drive, oil) {
  var _car = new Object();
  _car.color = color;
  _car.drive = drive;
  _car.oil = oil;
  _car.showColor = function() {
    console.log(this.color);
  }
  return _car;
}

var car1 = Car("red", 4 , 8);
var car2 = Car("blue", 2, 2);

car1.showColor(); // 'red'
car2.showColor(); // 'blue'

/*
  上面的代码就是简单的工厂模式类型使用 Car 类可以快速创建多个汽车实例它们的结构相同但是属性不同可以初始化不同的颜色驱动轮数油耗指标
/

可以把方法置于 Car() 函数外面,避免每次实例化时都要创建一次函数,让每个实例共享同一个函数。

function showColor() {
  console.log(this.color);
}

function Car(color, drive, oil) {
  var _car = new Object();
  _car.color = color;
  _car.drive = drive;
  _car.oil = oil;
  _car.showColor = showColor;
  return _car;
}

var car1 = Car("red", 4 , 8);
var car2 = Car("blue", 2, 2);

car1.showColor(); // 'red'
car2.showColor(); // 'blue'

类继承

在子类汇调用父类构造函数。

// 三重继承的案例,包括 基类、父类、子类,它们逐级继承。
// 基类 Base
function Base(x) {
  this.get = function() {
    return x;
  }
}

Base.prototype.has = function() { // 原型方法,判断 get() 返回值是否为 0
  return !(this.get() == 0)
}

// 父类
function Parent() {
   var a = [];
   a = Array.apply(a, arguments); // 把参数转化为数组
   Base.call(this, a.length); // 调用基类,并把参数数组长度传给他
   this.add = function() {
     return a.push.apply(a, arguments); // 把参数数组补加到数组 a 中并返回
   }
   this.geta = function() {
     return a; // 返回数组 a
   }
}

Parent.prototype = new Base();
Parent.prototype.constructor = Parent;
Parent.prototype.str = function() {
  return this.geta().toString(); // 把数组转化为字符串,并返回
}

// 子类 Sub
function Sub() {
  Parent.apply(this, arguments); // 调用 Parent 类,并把参数传给父类
  this.sort = function() {
    var a = this.geta(); // 获取数组值
    a.sort.apply(a, arguments); // 调用数组排序方法 sort() 对数组进行排序
  }
}

Sub.prototype = new Parent(); // 设置 Sub 原型为 Parent 实,建立原型链
Sub.prototype.constructor = Sub;

// 父类 Parent 的实例继承类 Base 的成员
var parent = new Parent(1, 2, 3, 4); // 实例化父类
console.log(parent.get()); // 4 (参数长度)
console.log(parent.has()); // true

// 子类 Sub 的实例继承类 Parent 和 lei Base 的成员
var sub = new Sub(30, 10, 20, 40);
sub.add(6, 5);
console.log(sub.geta()); // [30, 10, 20, 40, 6, 5]

sub.sort();

console.log(sub.geta()); // [10, 20, 30, 40, 5, 6]

console.log(sub.get()); // 4
console.log(sub.has()); // true
console.log(sub.str()); // 10,20,30,40,5,6

下面尝试把类继承模式封装起来,以便规范代码应用。

function extend(Sub, Sup) { // 子类, 父类
  var F = function() {};
  F.prototype = Sup.prototype;
  Sub.prototype = new F();
  Sub.prototype.constructor = Sub;
  Sub.sup = Sup.prototype; // 在子类定义一个私有属性存储父类原型
  // 检测父类原型构造器是否为自身
  if(Sup.prototype.constructor == Object.prototype.constructor) {
    Sup.prototype.constructor = Sup; // 类继承封装函数
  }
}

/*
  定义空函数 F,实现中转,设计它的原型为父类的原型,然后把空函数的实例,传递给子类的原型。这样就避免了直接实例化父类可能带来的系统负荷。因为在实际开发中,父类的规模可能很大,如果实例化,会占用大量内存。

  恢复子类原型的构造器为子类自己,同时,检测父类原型构造器是否与 Object 的原型构造器发生耦合,如果是,则恢复它的构造器为父类自己。
*/

下面定义两个类,尝试把他们绑定为继承关系:

function extend(Sub, Sup) { // 子类, 父类
  var F = function() {};
  F.prototype = Sup.prototype;
  Sub.prototype = new F();
  Sub.prototype.constructor = Sub;
  Sub.sup = Sup.prototype; // 在子类定义一个私有属性存储父类原型
  // 检测父类原型构造器是否为自身
  if(Sup.prototype.constructor == Object.prototype.constructor) {
    Sup.prototype.constructor = Sup; // 类继承封装函数
  }
}

function A(x) {
  this.x = x;
  this.get = function() {
    return this.x;
  }
}

A.prototype.add = function() {
  return this.x + this.x;
}

A.prototype.mul = function() {
  return this.x * this.x;
}

function B(x) {
  A.call(this, x);
}

extend(B, A); // 封装函数,将 A 和 B 的原型捆绑在一起

var f = new B(5);

console.log(f.get()); // 5

console.log(f.add()); // 10

console.log(f.mul()); // 25

模块化

模块就是提供一个接口,却隐藏状态与实现的函数或对象。一般在开发中使用闭包函数来构建模块,摒弃全局变量的滥用,规避 js 缺陷。

本例为 String 扩展一个 toHTML 原型方法,该方法能够把字符串中 HTML 转义字符替换为对应的字符。

Function.prototype.method = typeof Function.prototype.method === 'function' ? Function.prototype.method :
function (name, func) {
  if(!this.prototype[name]) {
    this.prototype[name] = func;
  }
  return this;
}

String.method('toHTML', function() {
  var entity = {
    quot: '"',
    lt: '<',
    gt: '>'
  };
  return function() {
    return this.replace(/&([^&;]+);/g, function(a, b) {
      var r = entity[b];
      return typeof r === 'string' ? r : a;
    })
  }
}());

console.log('&lt;&quot;&gt;'.toHTML()); // <">

模块开发的一般形式:一个定义了私有变量和函数的函数,利用闭包创建可以访问到的私有变量和函数的特权函数,最后返回这个特权函数,或者把他们保存到可访问的地方。

使用模块开发避免全局变量的滥用,从而保护信息的安全性。

实例:设计一个能够自动生产序列号的对象。

var toSerial = function() {
  var prefix = '';
  var serial = 0;
  return {
    setPrefix: function(p) {
      prefix = String(p);
    },
    setSerial: function (s) {
      serial = typeof s == 'number' ? s : 0;
    },
    get: function() {
      var result = prefix + serial;
      serial += 1;
      return result;
    }
  }
}

var serial = toSerial();

serial.setPrefix('No.');
serial.setSerial(100);
console.log(serial.get()); // No.100
console.log(serial.get()); // No.101
console.log(serial.get()); // No.102
console.log(serial.get()); // No.103
console.log(serial.get()); // No.104

案例实战

定义类型

JS 中,可以把构造函数理解为一个类型,这个类型是 JS 面向对象变成的基础。定义一个函数就相当于创建一个类型,然后借助这个类型来实例化对象。

下面定义一个空类型,类名为 jQuery

var jQuery = function() {
    // 函数体
}

下面为jQuery 扩展原型

var jQuery = function() {}
jQuery.prototype = {
    // 扩展的原型对象
}

jQuery 的原型起个别名:fn,如果直接命名为 fn,则表示它属于 window,这样不安全,更安全的做法是为 jQuery 类型对象定义一个静态引用 jQuery.fn,然后,把 jQuery 的原型对象传递给这个属性 jQuery.fn

jQuery.fn = jQuery.prototype = {
    // 扩展的原型对象
}
/*
  jQuery.fn 引用 jQuery.prototype,因此要访问 jQuery 的原型对象,可以使用 jQuery.fn。
*/

下面给 jQuery 类型起个别名:$

var $ = jQuery = function() {}
var $ = jQuery = function() {}
jQuery.fn = jQuery.prototype = {
    version: "3.2.1",
    size: function() {
        return this.length;
    }
}

返回 jQuery 对象

var $ = jQuery = function() {}
jQuery.fn = jQuery.prototype = {
    version: "3.2.1",
    size: function() {
        return this.length;
    }
}

var test = new $();

console.log(test.version); // "3.2.1"
console.log(test.size()); // undefined

但是 jQuery 框架是按照下面模式进行调用的,没有使用 new 命令。

var $ = jQuery = function() {
  return new jQuery();
}
jQuery.fn = jQuery.prototype = {
    version: "3.2.1",
    size: function() {
        return this.length;
    }
}

var test = new $();

// 报错内存溢出,说明在构造函数内部实例化对象是不允许的,因为会导致死循环
console.log($().version);
console.log($().size());

下面使用工厂模式进行设计:在 jQuery() 构造函数中返回 jQuery 的原型引用。

var $ = jQuery = function() {
  return jQuery.prototype;
}
jQuery.fn = jQuery.prototype = {
    version: "3.2.1",
    size: function() {
        return this.length;
    }
}

console.log($().version); // '3.2.1'
console.log($().size()); // undefined

上面基于 $().size() 这种形式的用法,但是在构造函数中直接返回原型对象,设计思路过于狭窄,无法实现框架内部的管理和扩展。下面模拟其他面向对象语言的设计模式:在类型内部定义一个初始化构造函数 init() ,当类型实例化后,直接执行初始化构造函数 init(),然后再返回 jQuery 的原型对象。

var $ = jQuery = function() {
  return jQuery.fn.init(); // 调用原型方法 init(),模拟类的初始化构造函数
}

jQuery.fn = jQuery.prototype = {
    init: function() {
      return this; // 返回原型对象
    },
    version: "3.2.1",
    size: function() {
        return this.length;
    }
}

console.log($().version); // '3.2.1'
console.log($().size()); // undefined

设计作用域

上面代码在使用过程汇总会发现一个问题:作用域混乱,给后期的扩展带来隐患。

定义jQuery 原型中包含一个 length 属性,同时初始化函数 init() 内部也包含一个 length 属性和一个 _size() 方法。

var $ = jQuery = function() {
  return jQuery.fn.init();
}

jQuery.fn = jQuery.prototype = {
   init: function() {
     this.length = 0; // 原型属性
     this._size = function() { // 原型方法
       return this.length;
     }
     return this;
   },
   length: 1,
   version: "3.2.1",
   size: function() {
     return this.length;
   }
}

console.log($().version); // '3.2.1'
console.log($()._size()); // 0
console.log($().size()); // 0

/*

  简单的概括:初始化函数 init() 的内、外作用域缺乏独立性,对于 jQuery 这样的框架来说,很可能造成消极影响。

*/

jQuery 框架是通过下面方式调用 init() 初始化函数的。

var $ = jQuery = function(selector, context) {
    return new jQuery.fn.init(selector, context); // 实例化 init()  分隔作用域
}
/*
  使用 new 命令调用初始化函数 init(),创建一个独立的实例对象,这样就分隔了 init() 函数内外的作用域,确保内外 this 引用不用。
*/
var $ = jQuery = function() {
  return new jQuery.fn.init();
}

jQuery.fn = jQuery.prototype = {
   init: function() {
     this.length = 0; // 原型属性
     this._size = function() { // 原型方法
       return this.length;
     }
     return this;
   },
   length: 1,
   version: "3.2.1",
   size: function() {
     return this.length;
   }
}

console.log($().version); // undefined
console.log($()._size()); // 0
console.log($().size()); // 抛出异常
/*
  运行报错:由于作用域被阻断,导致无法访问 jQuery.fn 对象的属性或方法。
*/

跨域访问

探索如何越过作用域访问,实现跨域访问外部的 jQuery.prototype

分析 jQuery 框架源码,发现它是通过原型传播解决这个问题的。实现方法:把 jQuery.fn 传递给 jQuery.fn.init.prototype,用 jQuery 的原型对象覆盖 init 的原型对象,从而实现跨域访问。

var $ = jQuery = function() {
  return new jQuery.fn.init();
}

jQuery.fn = jQuery.prototype = {
   init: function() {
     this.length = 0; // 本地属性
     this._size = function() { // 本地方法
       return this.length;
     }
     return this;
   },
   length: 1,
   version: "3.2.1", // 原型属性
   size: function() { // 原型方法
     return this.length;
   }
}

// 使用 jQuery 的原型对象覆盖 init 的原型对象
jQuery.fn.init.prototype = jQuery.fn;

console.log($().version); // "3.2.1"
console.log($()._size()); // 0
console.log($().size()); // 0

/*
  new jQuery.fn.init() 将创建一个新的实例对象,它拥有 init 类型的 prototype 原型对象,现在通过改变 prototype 指针,使其指向 jQuery 类的 prototype, 这样新实例实际上就继承了 jQuery.fn 原型对象成员。
*/

设计选择器

下面尝试为 jQuery 函数传递一个参数,并让它返回一个 jQuery 对象。

jQuery() 构造函数包含两个参数:selectorcontextselector 表示选择器,context 表示匹配的上下文,即可选择的范围,它表示一个 DOM 元素。为了简化操作,本例假设选择器的类型仅为标签选择器。

var $ = jQuery = function(selector, context) {
  return new jQuery.fn.init(selector, context);
}

jQuery.fn = jQuery.prototype = {
  init: function(selector, context) {
    selector = selector || document;
    context = context || document;
    console.log('this', this); // 空对象
    if (selector.nodeType) { // 如果是 dom 元素
      this[0] = selector; // 直接把该 dom 元素传递给实例对象的伪数组
      this.length = 1; // 设置实例对象的 length 属性,表示包含 1 个元素
      this.context = selector; // 重新设置上下文为 dom 元素
      return this;
    }
    if (typeof selector === 'string') { // 如果是选择符类型的字符串
      var e = context.getElementsByTagName(selector); // 获得指定名称的元素
      for (var i = 0; i < e.length; i++) { // 把所有元素传入到当前实例数组中
        this[i] = e[i];
      }
      this.length = e.length;
      this.context = context;
      return this; // 返回当前实例
    } else {
      this.length = 0; // 设置实例的 length 属性值为 0,表示不包含元素
      this.context = context; // 保存上下文对象
      return this; // 返回当前实例
    }
  }
}

jQuery.fn.init.prototype = jQuery.fn;

window.onload = function() {
  console.log($('div').length); // 3
  console.log($('div')); // init {0: div, 1: div, 2: div, 3: div, 4: div, 5: div, length: 6, context: document}
}

设计迭代器

var $ = jQuery = function(selector, context) {
  return new jQuery.fn.init(selector, context);
}

jQuery.fn = jQuery.prototype = {
  init: function(selector, context) {
    selector = selector || document;
    context = context || document;
    console.log('this', this); // 空对象
    if (selector.nodeType) { // 如果是 dom 元素
      this[0] = selector; // 直接把该 dom 元素传递给实例对象的伪数组
      this.length = 1; // 设置实例对象的 length 属性,表示包含 1 个元素
      this.context = selector; // 重新设置上下文为 dom 元素
      return this;
    }
    if (typeof selector === 'string') { // 如果是选择符类型的字符串
      var e = context.getElementsByTagName(selector); // 获得指定名称的元素
      for (var i = 0; i < e.length; i++) { // 把所有元素传入到当前实例数组中
        this[i] = e[i];
      }
      this.length = e.length;
      this.context = context;
      return this; // 返回当前实例
    } else {
      this.length = 0; // 设置实例的 length 属性值为 0,表示不包含元素
      this.context = context; // 保存上下文对象
      return this; // 返回当前实例
    }
  },
  html: function(val) {
    jQuery.each(this, function(val) {
      this.innerHTML = val;
    }, val)
  }
}

jQuery.fn.init.prototype = jQuery.fn;
jQuery.each = function(object, callback, args) {
  for (var i = 0; i < object.length; i++) {
    callback.call(object[i], args);
  }
  return object;
}

window.onload = function() {
  $('div').html("<h1>Hello</h1>");
}

设计扩展

jQuery 提供了良好的扩展接口,方便用户自定义 jQuery 方法,分析 jQuery 源码,会发现它是通过 extend() 函数来扩展的。

// 下面代码是 jQuery 框架通过 extend() 函数来扩展功能
jQuery.extend({ // 扩展工具函数
    noConflict: function(deep) {},
    isFunction: function(obj) {},
    isArray: function(obj){},
    isXMLDoc: function(elem) {},
    globalEval: function(data) {}
})
// 或者
jQuery.fn.extend({
    show: function(speed, callback) {},
    hide: function(speed, callback) {},
    toggle: function(fn, fn2) {},
    fadeTo: function(speed, to, callback) {},
    animate: function(prop, speed,easing, callback) {},
    stop: function(clearQueue, gotoEnd)
})
var $ = jQuery = function(selector, context) {
  return new jQuery.fn.init(selector, context);
}

jQuery.fn = jQuery.prototype = {
  init: function(selector, context) {
    selector = selector || document;
    context = context || document;
    console.log('this', this); // 空对象
    if (selector.nodeType) { // 如果是 dom 元素
      this[0] = selector; // 直接把该 dom 元素传递给实例对象的伪数组
      this.length = 1; // 设置实例对象的 length 属性,表示包含 1 个元素
      this.context = selector; // 重新设置上下文为 dom 元素
      return this;
    }
    if (typeof selector === 'string') { // 如果是选择符类型的字符串
      var e = context.getElementsByTagName(selector); // 获得指定名称的元素
      for (var i = 0; i < e.length; i++) { // 把所有元素传入到当前实例数组中
        this[i] = e[i];
      }
      this.length = e.length;
      this.context = context;
      return this; // 返回当前实例
    } else {
      this.length = 0; // 设置实例的 length 属性值为 0,表示不包含元素
      this.context = context; // 保存上下文对象
      return this; // 返回当前实例
    }
  }
}

jQuery.fn.init.prototype = jQuery.fn;

jQuery.each = function(object, callback, args) {
  for (var i = 0; i < object.length; i++) {
    callback.call(object[i], args);
  }
  return object;
}

// jQuery 扩展函数
jQuery.extend = jQuery.fn.extend = function(obj) {
  for (var prop in obj) {
    this[prop] = obj[prop];
  }
  return this;
}

// jQuery 对象扩展方法
jQuery.fn.extend({
  html: function(val) {
    jQuery.each(this, function(val) {
      this.innerHTML = val;
    }, val)
  }
})

window.onload = function() {
  $('div').html("<h1>Hello World</h1>");
}

传递参数

使用对象直接作为参数传递。

重新编写 jQuery.extend() 工具函数

<p>少年不知愁滋味</p>
<p>少年不知愁滋味</p>
<p>少年不知愁滋味</p>
<p>少年不知愁滋味</p>
var $ = jQuery = function(selector, context) {
  return new jQuery.fn.init(selector, context);
}

jQuery.fn = jQuery.prototype = {
  init: function(selector, context) {
    selector = selector || document;
    context = context || document;
    console.log('this', this); // 空对象
    if (selector.nodeType) { // 如果是 dom 元素
      this[0] = selector; // 直接把该 dom 元素传递给实例对象的伪数组
      this.length = 1; // 设置实例对象的 length 属性,表示包含 1 个元素
      this.context = selector; // 重新设置上下文为 dom 元素
      return this;
    }
    if (typeof selector === 'string') { // 如果是选择符类型的字符串
      var e = context.getElementsByTagName(selector); // 获得指定名称的元素
      for (var i = 0; i < e.length; i++) { // 把所有元素传入到当前实例数组中
        this[i] = e[i];
      }
      this.length = e.length;
      this.context = context;
      return this; // 返回当前实例
    } else {
      this.length = 0; // 设置实例的 length 属性值为 0,表示不包含元素
      this.context = context; // 保存上下文对象
      return this; // 返回当前实例
    }
  }
}

jQuery.fn.init.prototype = jQuery.fn;

jQuery.each = function(object, callback, args) {
  for (var i = 0; i < object.length; i++) {
    callback.call(object[i], args);
  }
  return object;
}

// 重新定义 jQuery 扩展函数
jQuery.extend = jQuery.fn.extend = function() {
  var destination = arguments[0],
    source = arguments[1]; // 获取第一个和第二个参数
  // 如果两个参数都存在,且都是对象
  if (typeof destination == 'object' && typeof source == 'object') {
    // 把第二个参数对合并到第一个参数对象中,并返回合并后的对象
    for (var property in source) {
      destination[property] = source[property];
    }
    return destination;
  } else { // 如果包含一个参数,则为 jQuery 扩展功能,把插件复制到 jQuery 原型对象上
    for (var property in destination) {
      this[property] = destination[property];
    }
    return this;
  }
}

// jQuery 扩展方法
jQuery.fn.extend({
  fontStyle: function(obj) {
    var defaults = {
      color: '#000',
      bgcolor: '#fff',
      size: '14px',
      style: 'normal'
    };
    defaults = jQuery.extend(defaults, obj || {});
    jQuery.each(this, function() {
      this.style.color = defaults.color;
      this.style.backgroundColor = defaults.bgcolor;
      this.style.fontSize = defaults.size;
      this.style.fontStyle = defaults.style;
    })
  }
})

window.onload = function() {
  $('p').fontStyle({
    color: '#fff',
    bgcolor: '#000',
    size: '24px'
  })
}

设计独立空间

当在页面中引入多个 JS 框架,或者编写大量的 js 代码,很难确保有些代码不会发生冲突,如果希望 jQuery 框架与其他代码完全隔离开,闭包体是一种最佳方式。

(function() {
  var $ = jQuery = function(selector, context) {
    return new jQuery.fn.init(selector, context);
  }

  jQuery.fn = jQuery.prototype = {
    init: function(selector, context) {
      selector = selector || document;
      context = context || document;
      console.log('this', this); // 空对象
      if (selector.nodeType) { // 如果是 dom 元素
        this[0] = selector; // 直接把该 dom 元素传递给实例对象的伪数组
        this.length = 1; // 设置实例对象的 length 属性,表示包含 1 个元素
        this.context = selector; // 重新设置上下文为 dom 元素
        return this;
      }
      if (typeof selector === 'string') { // 如果是选择符类型的字符串
        var e = context.getElementsByTagName(selector); // 获得指定名称的元素
        for (var i = 0; i < e.length; i++) { // 把所有元素传入到当前实例数组中
          this[i] = e[i];
        }
        this.length = e.length;
        this.context = context;
        return this; // 返回当前实例
      } else {
        this.length = 0; // 设置实例的 length 属性值为 0,表示不包含元素
        this.context = context; // 保存上下文对象
        return this; // 返回当前实例
      }
    }
  }

  jQuery.fn.init.prototype = jQuery.fn;

  jQuery.each = function(object, callback, args) {
    for (var i = 0; i < object.length; i++) {
      callback.call(object[i], args);
    }
    return object;
  }

  // 重新定义 jQuery 扩展函数
  jQuery.extend = jQuery.fn.extend = function() {
    var destination = arguments[0],
      source = arguments[1]; // 获取第一个和第二个参数
    // 如果两个参数都存在,且都是对象
    if (typeof destination == 'object' && typeof source == 'object') {
      // 把第二个参数对合并到第一个参数对象中,并返回合并后的对象
      for (var property in source) {
        destination[property] = source[property];
      }
      return destination;
    } else { // 如果包含一个参数,则为 jQuery 扩展功能,把插件复制到 jQuery 原型对象上
      for (var property in destination) {
        this[property] = destination[property];
      }
      return this;
    }
  }

  // 开放接口
  window.jQuery = window.$ = jQuery;

})(window)

// 在闭包体外部,直接引用 jQuery.fn.extend() 函数为 jQuery 扩展 fontStyle 插件。
jQuery.fn.extend({
  fontStyle: function(obj) {
    var defaults = {
      color: '#000',
      bgcolor: '#fff',
      size: '14px',
      style: 'normal'
    };
    defaults = jQuery.extend(defaults, obj || {});
    jQuery.each(this, function() {
      this.style.color = defaults.color;
      this.style.backgroundColor = defaults.bgcolor;
      this.style.fontSize = defaults.size;
      this.style.fontStyle = defaults.style;
    })
  }
})
// 使用插件
window.onload = function() {
  $('p').fontStyle({color: '#fff', bgcolor: '#ff0000', size: '24px'});
}

- Book Lists -