JavaScript是以对象为基础、函数为模型、原型为继承的基于对象的开发模式。JavaScript不是面向对象的编程语言,在ECMAScript 6规范之前,JavaScript没有类的概念,仅允许通过构造函数模拟类,通过原型实现继承。ECMAScript 6新增类和模块功能,提升了JavaScript高级编程的能力。
1、构造函数
构造函数(constructor)也称类型函数或构造器,功能类似于对象模板,一个构造函数可以生成任意多个实例,实例对象拥有相同的原型属性和行为特征。
1.1、定义构造函数
在语法和用法上,构造函数与普通函数没有任何区别。定义构造函数的方法如下:
function 类型名称( 配置参数 ) {
this.属性1 = 属性值1;
this.属性2= 属性值2;
…
this.方法1 = function(){
//处理代码
};
…
//其他代码,可以包含return语句
}
提示,建议构造函数的名称首字母大写,以便与普通函数进行区分。
注意:构造函数有两个显著特点:
- 函数体内可以使用this,引用将要生成的实例对象。当然,普通函数内也允许使用this,指代调用函数的对象。
- 必须使用new命令调用函数,才能够生成实例对象。如果直接调用构造函数,则不会直接生成实例对象,此时与普通函数的功能相同。
【示例】演示定义一个构造函数,包含两个属性和1个方法:
function Point(x,y){ //构造函数
this.x = x; //私有属性
this.y = y; //私有属性
this.sum = function(){ //方法
return this.x + this.y;
}
}
在上面代码中,Point是构造函数,它提供模板,用来生成实例对象。
1.2、调用构造函数
使用new命令可以调用构造函数,创建实例,并返回这个对象。
【示例】使用new命令调用构造函数,生成两个实例,然后分别读取属性,调用方法sum():
function Point(x,y){ //构造函数
this.x = x; //私有属性
this.y = y; //私有属性
this.sum = function(){ //私有方法
return this.x + this.y;
}
}
var p1 = new Point(100,200); //实例化对象1
var p2 = new Point(300,400); //实例化对象2
console.log(p1.x); //100
console.log(p2.x); //300
console.log(p1.sum()); //300
console.log(p2.sum()); //700
提示:构造函数可以接收参数,以便初始化实例对象。如果不需要传递参数,可以省略小括号,直接使用new命令调用,下面两行代码是等价的。
var p1 = new Point();
var p2 = new Point;
如果不使用new命令,直接使用小括号调用构造函数,这时构造函数就是普通函数,不会生成实例对象,this就代表调用函数的对象,在客户端指代window全局对象。
为了避免误用,最有效的方法是在函数中启用严格模式,这样在调用构造函数时,就必须使用new命令,否则将抛出异常。
function Point(x,y){ //构造函数
'use strict'; //启用严格模式
this.x = x; //私有属性
this.y = y; //私有属性
this.sum = function(){ //私有方法
return this.x + this.y;
}
}
或者使用if对this进行检测,如果this不是实例对象,则强迫返回实例对象。
function Point(x,y){ //构造函数
if(!(this instanceof Point)) return new Point(x, y); //检测this是否为实例对象
this.x = x; //私有属性
this.y = y; //私有属性
this.sum = function(){ //私有方法
return this.x + this.y;
}
}
1.3、构造函数的返回值
构造函数允许使用return语句。如果返回值为简单值,则将被忽略,直接返回this指代的实例对象;如果返回值为对象,则将覆盖this指代的实例,返回return语句后面的对象。
【示例】在构造函数内部定义return返回一个对象直接量,当使用new命令调用构造函数时,返回的不是this指代的实例,而是这个对象直接量,因此当读取x和y属性值时,与预期的结果是不同的:
function Point(x,y){ //构造函数
this.x = x; //私有属性
this.y = y; //私有属性
return { x : true, y : false }
}
var p1 = new Point(100,200); //实例化对象1
console.log(p1.x); //true
console.log(p1.y); //false
1.4、引用构造函数
在普通函数内,使用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是否为实例对象
this.x = x; //私有属性
this.y = y //私有属性
}
var p1 = new Point(100,200); //实例化对象1
console.log(p1.x); //100
注意:IE浏览器对其支持不是很完善,使用时要考虑兼容性。
1.5、使用this
this是由JavaScript引擎在执行函数时自动生成,存在于函数内的一个动态指针,指代当前调用对象。具体用法如下:
this[.属性]
如果this未包含属性,则传递的是当前对象。
下面简单总结this在5种常用场景中的表现,以及应对策略:
1.普通调用
【示例1】演示函数引用和函数调用对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,说明this代表sub_obj
如果把子对象sub_obj的func改为函数调用:
obj.sub_obj = {
name : "子对象sub_obj",
func : obj.func() //调用父对象obj的方法func
}
则函数中的this所代表的是定义函数时所在的父对象obj:
var who = obj.sub_obj.func;
console.log(who.name); //返回父对象obj,说明this代表父对象obj
2.实例化
【示例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; //实例化
3.动态调用
【示例3】使用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指向实例对象
func.call(1); //this指向数值对象
在上面示例中,直接调用函数func()时,this代表Window。当使用new命令调用函数时,将创建一个新的实例对象,this就指向这个新创建的实例对象。
使用call方法执行函数func()时,由于call方法的参数值为数字1,则JavaScript引擎会把数字1强制封装为数值对象,此时this就会指向这个数值对象。
4.事件处理
【示例4】在事件处理函数中,this总是指向触发该事件的对象:
<input type="button" value="测试按钮" />
<script>
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;
</script>
在上面代码中,func()所包含的this不再指向对象obj,而是指向按钮button,因为func()是被传递给按钮的事件处理函数之后才被调用执行的。
如果使用DOM 2级标准注册事件处理函数,代码如下:
if(window.attachEvent){ //兼容IE模型
button.attachEvent("onclick", obj.func);
} else{ //兼容DOM标准模型
button.addEventListener("click", obj.func, true);
}
在IE浏览器中,this指向Window和button,而在DOM标准的浏览器中仅指向button。因为,在IE浏览器中,attachEvent()是Window对象的方法,调用该方法时,this会指向Window。
为了解决浏览器的兼容性问题,可以调用call或apply方法强制在对象obj身上执行方法func(),以避免不同浏览器对this的解析不同:
if(window.attachEvent){
button.attachEvent("onclick", function(){ //用闭包封装call方法强制执行func()
obj.func.call(obj);
});
}
else{
button.addEventListener("click", function(){
obj.func.call(obj);
}, true);
}
当再次执行时,func()中包含的this始终指向对象obj。
5.定时器
【示例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);
在IE中this指向Window和Button对象,具体原因与上面讲解的attachEvent()方法相同。在符合DOM标准的浏览器中,this指向Window对象,而不是Button对象。
因为方法setTimeout()是在全局作用域中被执行的,所以this指向Window对象。解决浏览器兼容性问题,可以使用call或apply方法来实现。
setTimeout(function(){
obj.func.call(obj);
}, 100);
1.6、绑定函数
绑定函数是为了纠正函数的执行上下文,把this绑定到指定对象上,避免在不同执行上下文中调用函数时,this指代的对象不断变化。
function bind(fn, context) { //绑定函数
return function() {
return fn.apply(context, arguments); //在指定上下文对象上动态调用函数
};
}
bind()函数接收一个函数和一个上下文环境,返回一个在给定环境中调用给定函数的函数,并且将返回函数的所有的参数原封不动地传递给调用函数。
注意:这里的arguments属于内部函数,而不属于bind()函数。在调用返回的函数时,会在给定的环境中执行被传入的函数,并传入所有参数。
函数绑定可以在特定的环境中为指定的参数调用另一个函数,该特征常与回调函数、事件处理函数一起使用:
<button id="btn">测试按钮</button>
<script>
var handler = { //事件处理对象
message : 'handler', //名称
click : function(event) { //事件处理函数
console.log(this.message); //提示当前对象的message值
}
};
var btn = document.getElementById('btn');
btn.addEventListener('click', handler.click); //undefined
</script>
在上面示例中,为按钮绑定单击事件处理函数,设计当单击按钮时,将显示handler对象的message属性值。但是,实际测试发现,this最后指向了DOM按钮,而非handler。
解决方法:使用闭包进行修正:
var handler = { //事件处理对象
message : 'handler', //名称
click : function(event) { //事件处理函数
console.log(this.message); //提示当前对象的message值
}
};
var btn = document.getElementById('btn');
btn.addEventListener('click', function(){ //使用闭包进行修正:封装事件处理函数的调用
handler.click();
}); //'handler'
改进方法:使用闭包比较麻烦,如果创建多个闭包可能会令代码变得难于理解和调试,因此使用bind()绑定函数就很方便:
var handler = { //事件处理对象
message : 'handler', //名称
click : function(event) { //事件处理函数
console.log(this.message); //提示当前对象的message值
}
};
var btn = document.getElementById('btn');
btn.addEventListener('click', bind(handler.click, handler)); //'handler'
1.7、使用bind
ECMAScript 5为Function新增bind原型方法,用来把函数绑定到指定对象上。在绑定函数中,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
2、原型
在JavaScript中,所有函数都有原型,函数实例化后,实例对象可以访问原型属性,实现继承机制。
2.1、定义原型
原型实际上就是一个普通对象,继承于Object类,由JavaScript自动创建并依附于每个函数身上。使用点语法,可以通过function.prototype访问和操作原型对象。
【示例】为函数P定义原型:
function P(x){ //构造函数
this.x = x; //声明私有属性,并初始化为参数x
}
P.prototype.x = 1 //添加原型属性x,赋值为1
var p1 = new P(10); //实例化对象,并设置参数为10
P.prototype.x = p1.x //设置原型属性值为私有属性值
console.log(P.prototype.x); //返回10
2.2、访问原型
访问原型对象有3种方法,简单说明如下:
- 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)。
【示例】创建一个空的构造函数,然后实例化,分别使用上述3种方法访问实例对象的原型:
var F = function(){}; //构造函数
var obj = new F(); //实例化
var proto1 = Object.getPrototypeOf( obj ); //引用原型
var proto2 = obj.__proto__; //引用原型,注意,IE暂不支持
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( proto3 === proto4 ); //true
2.3、设置原型
设置原型对象有3种方法,简单说明如下:
- obj.proto = prototypeObj。
- Object.setPrototypeOf(obj,prototypeObj)。
- Object.create(prototypeObj)。
其中,obj表示一个实例对象,prototypeObj表示原型对象。注意,IE不支持前面两种方法。
【示例】简单演示原型对象的3种方法,为对象直接量设置原型:
var proto = { name:"prototype"}; //原型对象
var obj1 = {}; //普通对象直接量
obj1.__proto__ = proto; //设置原型
console.log( obj1.name);
var obj2 = {}; //普通对象直接量
Object.setPrototypeOf(obj2, proto); //设置原型
console.log( obj2.name);
var obj3 = Object.create(proto); //创建对象,并设置原型
console.log( obj3.name);
2.4、检测原型
使用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(/ /) ); //true
console.log( proto.isPrototypeOf(function(){}) ); //true
console.log( proto.isPrototypeOf(null) ); //false
2.5、原型属性
原型属性可以被所有实例访问,而私有属性只能被当前实例访问。
【示例】定义一个构造函数,并为实例对象定义私有属性:
function f(){ //声明一个构造类型
this.a = 1; //为构造类型声明一个私有属性
this.b = function(){ //为构造类型声明一个私有方法
return this.a;
};
}
var e =new f(); //实例化构造类型
console.log(e.a); //调用实例对象的属性a,返回1
console.log(e.b()); //调用实例对象的方法b,提示1
构造函数f中定义了两个私有属性,分别是属性a和方法b()。当构造函数实例化后,实例对象继承了构造函数的私有属性,此时可以在本地修改实例对象的属性a和方法b():
e.a = 2;
console.log(e.a);
console.log(e.b());
如果给构造函数定义了与原型属性同名的私有属性,则私有属性会覆盖原型属性值。
如果使用delete运算符删除私有属性,则原型属性会被访问。在上面示例基础上删除私有属性,则会发现可以访问原型属性。
2.6、原型链
在JavaScript中,实例对象在读取属性时,总是先检查私有属性,如果存在,则返回私有属性值,否则就会检索prototype原型。如果找到同名属性,则返回protoype原型的属性值。
protoype原型允许引用其他对象。如果在protoype原型中没有找到指定的属性,则JavaScript将会根据引用关系,继续检索protoype原型对象的protoype原型,以此类推。
【示例】演示对象属性查找原型的基本方法和规律:
function a(x){ //构造函数a
this.x = x;
}
a.prototype.x = 0; //原型属性x的值为0
function b(x){ //构造函数b
this.x = x;
}
b.prototype = new a(1); //原型对象为构造函数a的实例
function c(x){ //构造函数c
this.x = x;
}
c.prototype = new b(2); //原型对象为构造函数b的实例
var d = new c(3); //实例化构造函数c
console.log(d.x); //调用实例对象d的属性x,返回值为3
delete d.x; //删除实例对象的私有属性x
console.log(d.x); //调用实例对象d的属性x,返回值为2
delete c.prototype.x; //删除c类的原型属性x
console.log(d.x); //调用实例对象d的属性x,返回值为1
delete b.prototype.x; //删除b类的原型属性x
console.log(d.x); //调用实例对象d的属性x,返回值为0
delete a.prototype.x; //删除a类的原型属性x
console.log(d.x); //调用实例对象d的属性x,返回值为undefined
原型链能够帮助用户更清楚地认识JavaScript面向对象的继承关系,如下图所示:
3、类
3.1、定义类
在ECMAScript 5版本中,JavaScript是没有类这个概念的,其行为最接近的是创建一个构造函数,并在构造函数的原型上添加方法,这种方法也被称为自定义类型。
ECMAScript 6引入了Class(类)的概念,作为对象的模板,通过class关键字,可以定义类。class关键字必须小写,后面跟类名。
【示例】演示定义Person类,并为其添加一个属性和一个方法:
class Person{ //等效于Person()构造函数
constructor(name) { //类的构造函数,constuctor关键字小写
this.name = name; //设置属性
}
sayName() { //设置方法,等效于 Person.prototype.sayName
console.log(this.name);
}
}
constructor()是类的默认方法,通过new命令生成实例时,会自动调用该方法。一个类必须有constructor()方法,如果没有显式定义,默认会添加一个空的constructor()方法。constructor()方法默认会返回实例对象(this),也可以返回另一个指定对象。
ECMAScript 6为new命令引入了一个new.target属性,返回new命令作用于构造函数。如果构造函数不是通过new命令调用的,new.target将返回undefined。因此,在Class内部可以通过调用new.target访问当前Class。当子类继承父类时,new.target会返回子类。
提示:ECMAScript 6中的class类只是对ECMAScript 5中的构造函数做了一次封装,其语法格式与Java编程语言的类相似。
实例化类的方法与构造函数的实例化方法相同,都是使用new关键字。例如:
let person = new Person("小张");
person.sayName(); //输出“小张”
注意:关于类的定义和实例化,需要注意以下几点:
- 类声明和函数定义不同,类的声明是不会被提升的。类声明的行为与let比较相似。
- 类的所有方法都是不可枚举的,这与自定义类型相比是不同的,因为后者需要使用Object.defineProperty()才能定义不可枚举的方法。
- 所有方法都不能使用new调用。
- 不能使用new调用类的构造函数,必须使用类的实例化方式调用构造函数。
- 不允许在类的方法内部重写类名,因为在类的内部,类名是作为一个常量存在的。
3.2、继承
在Class中通过extends关键字可以实现继承,子类会继承父类的属性和方法。
【示例】定义两个类Father、Son,通过extends关键字让Son继承Father:
class Father{ //父类
constructor(name){ //构造函数
this.name = name;
}
sayName(){ //本地方法
console.log(this.name);
}
}
class Son extends Father{ //子类,extents后面指定要继承的类型
constructor(name, age){ //构造函数
super(name); //相当于以前的Father.call(this, name);
this.age = age;
}
sayAge(){ //子类的私有方法
console.log(this.age);
}
}
var son1 = new Son("李四", 30); //实例化
son1.sayAge(); //调用本地方法
son1.sayName(); //调用继承的方法
console.log(son1 instanceof Son); //返回true
console.log(son1 instanceof Father); //返回true
super()作为函数时,指向父类的构造函数。super作为对象时,指向父类的原型对象。
super代表父类的构造函数,但是返回的是子类的实例,即super内部的this指的是子类,因此super()相当于A.prototype.constructor.call(this)。
使用super()时要注意以下几点:
- 只能在子类的构造函数中使用super(),否则将抛出异常。
- 必须在构造函数的起始位置调用super(),因为它会初始化this,任何在super()之前访问this的行为都会造成错误,即super()必须放在构造函数的首行。因为子类实例的构建,是基于对父类实例的加工,只有super()方法才能返回父类实例。
- 在类的构造函数中,唯一能避免调用super()的办法是返回一个对象。
提示:ECMAScript 5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ECMAScript 6的继承机制完全不同,实质是先创造父类的实例对象this,所以必须先调用super()方法,然后再用子类的构造函数修改this。如果子类没有定义constructor()方法,这个方法会被默认添加。
3.3、静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类调用,也被称为静态方法。父类的静态方法,可以被子类继承。
【示例】为类定义静态方法:
class Father{ //父类
static foo(){ //定义静态方法
console.log("我是父类的静态方法");
}
}
class Son extends Father{ //子类,继承自Father
}
//子类继承了父类的静态方法
在子类身上调用父类的静态方法与直接通过父类名调用是一样的。
4、模块
JavaScript一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。当开发大型的、复杂的项目时,会非常麻烦。
在ECMAScript 6之前,主要使用CommonJS和AMD模块加载方案,前者用于服务器,后者用于浏览器。ECMAScript 6实现了模块功能,主要由两个命令构成,简单说明如下:
- export命令:显式指定输出的代码,用于用户自定义模块,规定对外接口。
- import命令:用于输入其他模块提供的功能,同时创造命名空间(namespace),防止函数名冲突。
【示例】演示如何使用JavaScript模块功能:
1.foo.js
export let counter = 3; //指定输出变量
export function inc(){ //指定输出函数
counter++;
}
2.main.js
import {counter, inc} from 'foo'; //从foo.js文件中导入变量counter和函数inc
console.log(counter); //输出3
inc(); //调用外部函数
console.log(counter); //输出4
- 使用export命令导出对象时,这个关键字可以无限次使用。
- 使用import命令将其他模块导入指定模块时,可用来导入任意数量的模块。
- 支持模块的异步加载,同时为加载模块提供编程支持。