js构造函数、原型与继承深入

构造函数、原型、继承

js是基于对象,但不完全面向对象的编程语言。在面向对象的编程模式中,有两个核心概念对象。在ECMAScript6规范之前,js没有类的概念,仅允许通过构造函数来模拟类,通过原型实现继承。

学习重点

  • 理解构造函数和this。
  • 定义js类型。
  • 正确使用原型继承。
  • 能够设计基于对象的web应用程序。

构造函数

js构造函数(Constructor)也称为构造器、类型函数,功能类似对象模板,一个构造函数可以生成任意多个实例,实例对象具有相同的属性、行为特性,但不相等。

定义构造函数

在语法和用法上,构造函数和普通函数没有任何区别。定义构造函数的方法如下:

function 类型名称(配置参数) {
    
    
	this.属性1 = 属性1;
  this.属性2 = 属性2;
  this.属性3 = 属性3;
  ...
  this.方法1 = function(){
    
    
  	//处理代码
  };
  //其他代码,可以包含return语句
}
  • 提示:建议构造函数的名称首字母大写,以便与普通函数进行区分。
  • 注意:构造函数有两个显著特点:
    • 函数体内使用this,引用将要生成的实例。
    • 必须使用new命令调用函数,生成实例对象。
  • 实例】下面实例演示定义一个构造函数,包含了两个属性和一个方法
function Point(x, y) {
    
    
    this.x = x;
    this.y = y;
    this.sum = function () {
    
    
        return this.x + this.y;
    }
}

在上面代码中,Point就是构造函数,它提供模板,用来生成实例对象。

调用构造函数

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

  • 注意:如果不适用 new命令,直接使用小括号调用构造函数,这时构造函数就是普通函数,不会生成实例对象,this就代表调用函数的对象,在客户端指代全局对象window。
  1. 为了避免误用,最有效的方法是在函数中启用严格模式
function Point(x, y) {
    
    
    'use strict';
    this.x = x;
    this.y = y;
    this.sum = function() {
    
    
        return this.x + this.y;
    }
}

这样调用构造函数时,必须使用new命令,否则将抛出异常。

  1. 或者使用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后跟随的对象

为什么会出现这种情况?这与new命令解析过程有关系,使用new命令调用函数的解析过程如下:

  1. 当使用new命令调用函数时,先创建一个空对象,作为实例返回。
  2. 设置实例的原型,指向构造函数的prototype属性。
  3. 设置构造函数体内的this值,让它指向实例。
  4. 开始执行构造函数内部代码。
  5. 如果构造函数内部有return语句,而且return后面跟着一个对象,会返回return语句指定的对象;否则会忽略return返回值,直接返回this对象。
  • 实例】下面实例再构造函数内部定义return返回一个对象直接量,当使用new命令调用构造函数时,返回的不是this指代的实例,而是这个对象直接量,因此当读取x和y属性值时,与预期结果是不同的。
function Point(x, y) {
    
    
    this.x = x;
    this.y = y;
    return {
    
    
        x: true,
        y: false
    };
}
var p1 = new Point(200, 200);
console.log(p1.x); // true
console.log(p1.y); // false

引用构造函数

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

  • 实例】下面实例在构造函数内使用new.target指代构造函数本身,以便对用户操作进行监测,如果没有使用new命令,则强迫使用new实例化。
function Point(x, y) {
    
    
    'use strict';
    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.x); // 100

this指针

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

this[.属性]

如果this未包含属性,则传递的是当前对象。

this用法灵活,其包含的值也是变化多端的。例如,下面实例使用call()方法不断改变函数内this指代对象。

var x = "window";

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

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

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

function f() {
    
    
    console.log(this.x);
}
f(); //window
f.call(window); //window
f.call(new a()); //a
f.call(new b()); //b
f.call(c); //undefined

下面简单总结this在5中常用场景中的表现以及应对策略。

  1. 普通调用
  • 实例1】下面实例演示了函数引用和函数调用对this的影响。
var obj = {
    
    
    name: "父对象obj",
    func: function() {
    
    
        return this;
    }
}
obj.sub_obj = {
    
    
    name: "子对象sub_obj",
    func: obj.func
}
var who = obj.sub_obj.func();
console.log(who.name); //子对象sub_obj

如果把子对象sub_obj的func改为函数调用。则函数中的this所代表的是定义函数时所在的父对象obj

var obj = {
    
    
    name: "父对象obj",
    func: function() {
    
    
        return this;
    }
}
obj.sub_obj = {
    
    
    name: "子对象sub_obj",
    func: obj.func()
}
var who = obj.sub_obj.func;
console.log(who.name); //父对象obj
  1. 实例化
  • 实例2】使用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 = 实例对象
  1. 动态调用
  • 实例3】使用call和apply可以强制改变this,使其指向参数对象。
func.call(1); // this指向数值对象

使用call方法执行函数func()时,由于call()方法的参数值为数字1,则js引擎会把数字1强制封装成数值对象,此时this就会指向这个数值对象。

  1. 事件处理
  • 实例4】在事件处理函数中,this总是指向触发该事件的对象。
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script>
        window.onload = function() {
    
    
            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
        }
    </script>
</head>

<body>
    <input type="button" value="测试按钮">
</body>

</html>

在上面代码中,func()所包含的this不再指向对象obj,而是指向按钮button,因为func()是被传递给按钮的事件处理函数之后才被调用执行的。

  1. 定时器
  • 实例5】使用定时器调用函数
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); // this = window

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

因为方法setTimeout()是在全局作用域中被执行的,所以this指向的是window对象。

this安全策略

由于this的不确定性,会给开发带来很多风险,因此使用this时,应时刻保持谨慎。锁定this有以下两种基本方法

  • 使用私有变量存储this。

  • 使用call和apply强制固定this的值。

  • 实例1】使用this作为参数来调用函数,可以避免产生this隐环境变化而变化的问题。

例如,下面做法是错误的,因为this会始终指向window对象,而不是当前按钮对象。

<input type="button" value="按钮1" onclick="func()">
<input type="button" value="按钮2" onclick="func()">
<input type="button" value="按钮3" onclick="func()">	
<script>
	function func() {
     
     
		console.log(this.value);
	}
</script>

如果把this作为参数进行传递,那么它就会代表当前对象。

<input type="button" value="按钮1" onclick="func(this)">
<input type="button" value="按钮2" onclick="func(this)">
<input type="button" value="按钮3" onclick="func(this)">	
<script>
	function func() {
    
    
		console.log(this.value);
	}
</script>
  • 实例2】使用私有变量存储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();
console.log(_this.name); //Base
  • 实例3】使用call和apply强制固定this的值。

作为一个动态指针,this也可以被转换为静态指针。实现方法:使用call()或apply()方法强制指定this的指代对象。

Function.prototype.pointTo = function(obj) {
    
    
    var _this = this;
    return function() {
    
    
        return _this.apply(obj, arguments);
    }
}

var obj1 = {
    
    
    name: "this = obj1"
}
obj1.func = (function() {
    
    
    return this;
}).pointTo(obj1);
var obj2 = {
    
    
    name: "this = obj2",
    func: obj1.func
}
var _this = obj2.func();
console.log(_this.name); //this = obj1

为Function扩展一个原型方法pointTo(),该方法将在指定的参数对象上调用当前函数,从而把this绑定到指定对象上。利用扩展,实现强制指定对象obj1的方法func()中的this始终指向obj1。

绑定函数

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

  • 代码实现
function bind(fn, context) {
    
    
    return function () {
    
    
        return fn.apply(context, arguments);
    }
}

bind()函数接受一个函数和一个上下文环境,返回一个在给定环境中调用给定函数的函数,并且将返回函数的所有的参数原封不动的传递给调回函数。

  • 注意:这里的arguments属于内部函数,而不属于bind()函数。在调用返回的函数时,会在给定的环境中执行被传入的参数,并传入所有参数。
  • 应用代码

函数绑定可以在特定的环境中为指定的参数调用另一函数,该特征常与回调函数、事件处理函数一起使用。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script>
        window.onload = function() {
     
     
            var handler = {
     
     
                message: 'handler',
                click: function(event) {
     
     
                    console.log(this.message);
                }
            };
            var btn = document.getElementById('btn');
            btn.addEventListener('click', handler.click);  //undefined
        }
    </script>
</head>
<body>
    <button id='btn'>测试按钮</button>
</body>
</html>

在上面实例中,为按钮绑定单击事件处理函数,设计当单机按钮时,将显示handler对象的message属性值。但是,实际测试发现,this最后指向了DOM按钮,而不是handler。

解决方法:使用闭包进行修正。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script>
        window.onload = function() {
     
     
            var handler = {
     
     
                message: 'handler',
                click: function(event) {
     
     
                    console.log(this.message);
                }
            };
            var btn = document.getElementById('btn');
            btn.addEventListener('click', function() {
     
     
                handler.click();
            }); //handler
        }
    </script>
</head>

<body>
    <button id='btn'>测试按钮</button>
</body>

</html>

改进方法:使用闭包比较麻烦,如果常见多个闭包可能会令代码变得难以理解和调试,而使用bind()绑定函数就很方便。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script>
        window.onload = function() {
     
     
            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)); //handler
        }
    </script>
</head>

<body>
    <button id='btn'>测试按钮</button>
</body>

</html>

使用bind

ECMAScript5为Function新增了bind原型方法,用来把函数绑定到指定对象。在绑定函数中,this对象被解析为传入的对象,this对象被解析为传入的对象。具体用法如下:

function.bind(thisArg[,arg1[,arg2[,argN]]]);

参数说明

  • function:必须参数,一个函数对象。
  • thisArg:必须参数,this可在新函数中引用的对象。
  • arg1[,arg2[,argN]]:可选参数,要传递到新函数的参数列表。

bind()方法将返回与function函数相同的新函数,thisArg对象和初始参数除外。

  • 实例1】下面实例定义原始函数check,用来检测输入的参数值是否在一个指定范围内,返回下限和上线根据当前实例对象的min和max属性决定。然后使用bind()方法把check函数绑定到对象range身上。如果再次调用这个新绑定后的函数check1,就可以根据该对象的属性min和max来确定调用函数时传入值是否在指定的范围内。
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
  • 实例2】在上面实例基础上,下面实例为obj对象定义了两个上下限属性,以及一个方法check。然后,直接调用obj对象的check方法,检测10是否在指定范围,返回值false,因为当前min和max值分别为50和100.接着把obj.check方法绑定到range对象,再次传入值10,返回值为true,说明在指定范围,因为此时min和max分别为10和20。
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);
var result = check1(10);
console.log(result); //true
  • 实例3】下面实例演示了如何使用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框架最大亮点就是它的链式语法。实现方法:设计每一个方法的返回值都是jQuery对象(this),这样调用方法的返回值可以为下一次调用其他方法做准备。

  • 实例】下面实例演示如何在函数中返回this来设计链式语法。分别为String扩展了3个方法,trim、writeln和log,其中writeln和log方法返回值都是this,而trim方法返回值为修剪后的字符串。这样就可以用链式语法在一行语句中快速调用3个方法
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();

原型

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

定义原型

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

  • 提示:Object和Function是两个不同类型的构造函数,利用运算符new可以创建不同的实例对象。

  • 实例】在下面代码中为函数P定义原型。

function P(x) {
    
    
	this.x = x;
}
P.prototype.x = 1;
var p1 = new P(10);
p.prototype.x = p1.x;
console.log(P.prototype.x);//10

访问原型

访问原型对象有三种方法:

  • obj.__proto__
  • obj.constructor.prototype
  • Object.getPrototypeOf(obj)

其中,obj表示一个实例对象,constructor表示构造函数。__proto__(前后各两个下划线)是一个私有属性,可读可写,与prototype属性相同,都可以访问原型对象。Object.getPrototypeOf(obj)是一个静态函数,参数为实例对象,返回值是参数对象的原型对象。

  • 注意__proto__属性是一个私有属性,存在浏览器兼容问题,以及缺乏费浏览器环境的支持。使用obj.constructor.prototype也存在一定的风险,如果obj对象的constructor属性值被覆盖,则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;
console.log(proto1 === proto2); //true
console.log(proto1 === proto3); //true
console.log(proto1 === proto4); //true
console.log(proto2 === proto3); //true
console.log(proto2 === proto4); //true
console.log(proto4 === proto4); //true

设置原型

设置原型对象有三个方法

  • obj.__proto__ == prototypeObj
  • Object.setPrototypeOf(obj, prototypeObj)
  • Object.create(prototypeObj)

其中,obj表示一个实例对象,prototypeObj表示原型对象。

  • 实例】下面代码简单演示上述三种方法为对象直接量设置原型。
var proto = {
    
    
    name: "prototype"
};
var obj1 = {
    
    };
obj1.__proto__ = proto;
console.log(obj1.name); //prototype
var obj2 = {
    
    };
Object.setPrototypeOf(obj2, proto);
console.log(obj2.name); //prototype
var obj3 = Object.create(proto);
console.log(obj3.name); //prototype

检测原型

使用isPrototypeOf()方法可以判断该对象是否为参数对象的原型。isPrototypeOf()是一个原型方法,可以在每个实例对象上调用。

  • 实例】下面代码简单演示如何检测原型对象。
var F = function() {
    
    };
var obj = new F();
var proto1 = Object.getPrototypeOf(obj);
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

原型属性和私有属性

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

  • 实例1】下面实例,演示如何定义一个构造函数,并为实例对象定义私有属性。
function f(){
    
    
	this.a = 1;
	this.b = function(){
    
    
		return this.a;
	};
}
var e = new f();
console.log(e.a);
console.log(e.b());

构造函数f中定义了两个私有属性,分别是属性a和方法b()。当构造函数实例化后,实例对象继承了构造函数的私有属性。此时可以在本地修改实例对象的属性a和方法b()。

e.a = 2;
console.log(e.a);
console.log(e.b());

如果给构造函数定义了与原型属性同名的私有属性,则私有属性会覆盖原型属性值

如果使用delete运算符删除私有属性,则原型属性会被访问。在上面实例的 基础上删除私有属性则会发现可以访问原型属性。

  • 实例2】私有属性可以在实例对象中被修改,不同实例对象之间不会相互干扰。
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); //1

上面实例演示了如何使用私有属性,则实例对象之间就不会相互影响。但是如果希望统一修改实例对象中包含的私有属性值,就需要一个个的修改,工作量会很大。

  • 实例3】原型属性将会影响所有实例对象,修改任何原型属性值,则该函数的所有实例都会看到这种变化,这样就省去了私有属性修改的麻烦。
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 f(){
    
    }
f.prototype.a = 1;
f.prototype.b = function(){
    
    
	return f.prototype.a;
}

prototype属性属于构造函数,所以必须使用构造函数通过点语法来调用prototype属性,在通过prototype属性来访问原型属性。

  • 实例4】利用对象原型与私有属性之间的这种特殊关系可以设计有趣演示效果。
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

上面实例定义了构造函数p,声明了3个私有属性,并实例化构造函数,把实例对象赋值给构造函数的原型方法。同时定义了原型方法del(),该方法将删除实例对象的所有属性和方法。最后,分别调用属性x、y、z,返回的是私有属性,调用方法del(),删除所有私有属性,再次调用私有属性x、y和z则返回的是原型属性值。

应用原型

下面通过几个实例介绍原型在代码中的应用技巧

  • 实例1】利用原型可以为对象设置默认值。当原型属性与私有属性同名时,删除私有属性之后,可以访问原型属性,既可以把原型属性值作为初始化默认值。
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) //1
  • 实例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
  • 实例3】利用原型还可以为对象设置“只读”属性,这在一定程序上可以避免对象内部数据被任意修改的问题。下面实例演示了如何根据平面上两点坐标来计算他们之间的距离。构造函数p可以用来设置定位点坐标,当传递两个参数值时,会返回以参数为坐标值的点。如果省略参数则默认为原点(0,0)。而遭构造函数l中通过传递两点坐标对象计算它们的距离。
function p(x, y) {
    
    
    if (x) {
    
    
        this.x = x;
    }
    if (y) {
    
    
        this.y = y;
    }
    p.prototype.x = 0;
    p.prototype.y = 0;
}

function l(a, b) {
    
    
    var a = a;
    var b = b;
    var w = function() {
    
    
        return Math.abs(a.x - b.x);
    };
    var h = function() {
    
    
        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);
var l1 = new l(p1, p2);
console.log(l1.length()); //20.12461179749811
l1.b().x = 50;
console.log(l1.length()); //43.86342439892262
  • 实例4】利用原型进行批量复制。
function f(x) {
    
    
    this.x = x;
}
var a = [];
for (var i = 0; i < 100; i++) {
    
    
    a[i] = new f(10);
}

上面代码将复制100次同一个实例,如果后期要修改数组中每个实例对象就会非常麻烦。现在可以尝试使用原型进行批量复制操作。

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();
}

把构造类f的实例存储在临时构造类的原型对象中,然后通过临时构造类temp实例来传递复制的值。这样,想要修改数组的值,只需要修改类f的原型即可,从而避免了逐一修改数组中的每个元素。

原型链

在js中,实例对象在读取属性时总是先检查私有属性。如果存在,则会返回私有属性值;否则就会检索prototype原型;如果找到同名属性,则会返回prototype原型的属性值。

prototype原型允许引用其他对象。如果在prototype原型中灭有找到指定的属性,则js将会根据引用关系,继续检索prototype原型对象的prototype原型,以此类推。

  • 实例】在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");
    }
}
console.log(f instanceof Function); //true
console.log(f.prototype instanceof Object); //true
console.log(Function instanceof Object); //true
console.log(Function.prototype instanceof Object); //true
console.log(Object instanceof Function); //true
console.log(Object.prototype instanceof Function); //false

原型继承

原型继承是一种简化的基层机构,也是js原生支持的继承模式。在原型继承中,类和实力概念被淡化了,一切都从对象的角度来考虑。原型继承不再需要继续使用类来定义结构,直接定义对象,并被其他对象引用,这样就形成了一种继承关系,其中引用对象被称为原型对象。js能够根据原型链来查找对象之间的这种继承关系。

  • 原型继承的优点是结构简练,使用简便,但是也存在以下几个缺点
  • 每个类型只有一个原型,所以它不支持多继承
  • 不能友好的支持带参数的父类。
  • 使用不灵活。在原型声明阶段实例化父类,并把它作为当前类型的原型,这限制了父类实例化的灵活性,无法确定父类实例化的时机和场合。
  • prototype属性固有的副作用。

扩展原型方法

js通过prototype为原生类型扩展方法,扩展方法可以被所有对象调用。例如,通过Function.prototype为函数扩展方法,然后为所有函数调用。

  • 实现代码】为Function添加一个原型方法method,该方法可以为其他类型添加原型方法。
Function.prototype.method = function (name, func) {
    
    
    this.prototype[name] = func;
    return this;
};
  • 代码应用
  • 实例1】下面利用method扩展方法,为Number扩展一个int原型方法。该方法可以对浮点数进行取整。
Number.method('int', function() {
    
    
    return Math[this < 0 ? 'ceil' : 'floor'](this);
});
console.log((-10 / 3).int()); //-3

Number.method方法能够根据数字的正负来判断是使用Math.ceil还是Math.floor,这样就不需要每次都编写上面的代码。

  • 实例2】下面利用method扩展方法为String扩展一个trim原型方法。该方法可以清除字符串左右两侧的空字符。
String.method('trim', function() {
    
    
    return this.replace(/^\s+|\s+$/g, '');
});
console.log('"' + " abc ".trim() + '"');

trim方法使用了一个正则表达式,把字符串中的左右两侧的空格符清除。

  • 注意:通过为原生的类型扩展方法,可以大大提高js编程的灵活性。但是在扩展基类时务必小心,避免覆盖原生方法。建议在覆盖之前先确定是否已经存在该方法。
Function.prototype.method = function(name, func) {
    
    
    if (!this.prototype[name]) {
    
    
        this.prototype[name] = func;
        return this;
    }
};

另外,可以使用hasOwnProperty方法过滤原型属性或者私有属性。

类型

js是以对象为基础以函数为模型以原型为继承的面向对象开发模式。

构造原型

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

  • 由于构造函数事先声明,而原型属性在类结构声明之后才被定义,因此无法通过构造函数参数向原型动态传递参数。这样实例化对象都是一个模样,没有个性。要改变原型属性值,则所有实例都受到干扰。

  • 当原型属性的值为引用类型数据时,如果在一个对象实例中修改该属性值,将会影响所有的实例。

  • 实例1】简单定义Book类型,然后实例化。

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的值发生变化,将会影响所有实例。

构造原型正是为了解决原型模式而诞生的一种混合设计模式,它把构造函数模式与原型模式混合使用,从而避免了上述问题的发生。

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

  • 实例2】遵循上述设计原则,把其中两个属性设计为该函数模式,设计方法为原型模式。
function Book(title, pages) {
    
    
    this.title = title;
    this.pages = pages;
}
Book.prototype.what = function() {
    
    
    console.log(this.title + this.pages);
};
var book1 = new Book("JavaScript程序设计", 160);
var book2 = new Book("c程序设计", 240);
console.log(book1.title); //JavaScript程序设计
console.log(book2.title); //c程序设计

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

动态原型

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

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.prototype.what = function() {
    
    
            console.log(this.title + this.pages);
        };
        Book.isLock = true;
    }
}
var book1 = new Book("JavaScript程序设计", 160);
var book2 = new Book("c程序设计", 240);
console.log(boo1.title);
console.log(book2.title);

typeof Book.isLock表达式能够检测该属性值的类型,如果返回为undefined字符串,则不会存在该属性值,说明没有创建原型方法,并允许创建原型方法,设置该属性的值为true,这样就不用重复创建原型方法。这里类名Book,而没有使用this,这是因为原型属性时属于本身的,而不是对象实例的。

  • 提示:动态原型模式与构造原型模式在性能上是等价的,用户可以自由选择,不过构造原型模式应用比较广泛

工厂模式

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

  • 实例1】下面实例设计一个Car类型:包含汽车颜色、驱动轮数、百公里油耗3个属性,同时定义一个方法,用来显示汽车颜色。
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, 6);
car1.showColor(); //red
car2.showColor(); //blue

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

  • 实例2】在类型中,方法就是一种行为或操作,它能够根据初始化参数完成特定任务,具有共性。因此,可以考虑吧方法置于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, 6);
car1.showColor(); //red
car2.showColor(); //blue

在上面这段重写的代码中,在函数Car()之前定义了函数showColor()。在Car()内部,通过引用外部showColor()函数,避免了每次实例化时都要创建一个新的函数。从功能上讲,这样解决了重复创建函数的问题;但是从语义上讲,该函数不太像是对象的方法。

类继承

类继承的设计方法:在子类中调用父类构造函数。

在js中实现类继承,需要注意下面3个技术问题。

  • 在子类中,使用apply调用父类,把子类构造函数的参数传递给父类构造函数。让子类继承父类的私有属性,即Parent.apply(this, argumetns);代码行。
  • 在父类和子类之间建立原型链,即Sub.prototype = new Parent();语句行。通过这种方式保证父类和子类是原型链上的上下级关系,即子类的prototype指向父类的一个实例。
  • 恢复子类的原型对象的构造函数,即Sub.prototype.constructor = Sub;语句行。当改动prototype原型时,就会破坏原来的constructor指针,所以必须重置constructor。
  • 实例1】下面实例演示了一个三重继承的案例,包括基类、父类和子类,他们逐级继承。
function Base(x) {
    
    
    this.get = function() {
    
    
        return x;
    }
}
Base.prototype.has = function() {
    
    
    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);
    }
    this.geta = function() {
    
    
        return a;
    }
}

Parent.prototype = new Base();
Parent.prototype.constructor = Parent;
Parent.prototype.str = function() {
    
    
    return this.geta().toString();
}

Sub.prototype = new Parent();
Sub.prototype.constructor = Sub;

var parent = new Parent(1, 2, 3, 4);
console.log(parent.get());
console.log(parent.has());

var sub = new Sub(30, 10, 20, 40);
sub.add(6, 5);
console.log(sub.geta());
sub.sort();
console.log(sub.geta());
console.log(sub.get());
console.log(sub.has());
console.log(sub.str());
  • 设计思路

设计子类Sub继承父类Parent,而父类Parent又继承基类Base。Base、Parent、Sub三个类之间的继承关系是通过在子类中调用父类的构造函数来维护的。

例如,在Sub子类中,Parent.apply(this, arguments);能够在子类中调用父类,并把子类的参数传递给父类,从而使子类具有父类的所有属性。

同理,在父类中,Base.call(this, a.length);把父类的参数长度作为值传递给基类,并进行调用,从而实现父类拥有基类的所有成员。

从继承上看,父类继承了基类的私有方法get(),为了确保能够继承基类的原始方法,还需要为他们建立原型链,从而实现原型对象的继承关系,方法是添加语句行:Parent.prototype= new Base();

同理,在子类中添加语句Sub.prototype = new Parent();这样通过原型链就可以把基类、父类和子类串联在一起,从而实现子类能够继承父类属性,还可以继承基类的属性。

猜你喜欢

转载自blog.csdn.net/weixin_46351593/article/details/108657593
今日推荐