构造函数原型继承
构造函数
定义构造函数
再语法和用法上,构造函数和普通函数没有任何区别。
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 对象
下面总结 this
在 5
种常用场景中的表现以及应对策略
- 普通调用
下面演示函数引用和函数调用对 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_obj
的 func
改为函数调用
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 = 实例对象
- 动态调用
使用 call
和 apply
可以强制改变 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
对象。要解决浏览器兼容性问题,可以使用 call
或 apply
方法来实现。
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
有以下两种基本方法:
- 使用私有变量存储
this
- 使用
call
和apply
强制固定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" }
使用 call
和 apply
强制固定 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
原型设计类的继承存在两个问题:
- 由于构造函数事先声明,而原型属性在类的结构声明之后才被定义,因此无法通过构造函数参数向原型动态传参。这样实例化对象都是一个模样,没有个性,要改变原型属性值,则所有实例都会受到干扰。
- 当原型属性的值为引用类型数据时,如果在一个对象实例中修改该属性值,将会影响所有的实例。
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('<">'.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()
构造函数包含两个参数:selector
和 context
。selector
表示选择器,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'});
}