一、概述
面向对象程序编程(Object-oriented programming,缩写:OOP)是用抽象方式构建基于现实世界模型的一种编程模式,JavaScript是一种基于对象(object-based)的语言,支持面向对象编程与函数式编程,但JavaScript的面向对象与其它的面向对象语言有较大差异,ECMAScript中没有类的概念,所以对象也有所不一样。
本章主要让讲解JavaScript中对象、原型与函数间的关系及面向对象编程相关内容。
二、对象
JavaScript中的一切都是对象,万物皆为对象,函数也是对象,要学习JavaScript面向对象编程需要先理解对象。
对象的定义是无序属性的集合,其属性可以包含基本值、对象或函数。通俗说对象就一个键值对集合,键是名称,值可以是数据或函数。
2.1、创建对象
JavaScript中有大量的内置对象,为开发提供了方便,但面向对象编程我们需要将现实世界抽象成自定义的对象,这里介绍多种对象的创建方式。
(1)、对象字面量
对象字面量是一种最直接最简单的对象创建方式,一个对象字面量就是包含在一对花括号中的零个或多个"键/值"对。对象字面量可以出现在任何允许表达式出现的地方。
//空对象
var obj1={};
//对象中的属性,如果属性名有空格需要使用引号
var obj2={name:"foo",age:19,"nick name":"bar"};
//对象中的方法
var obj3={
'price':99, //属性名可以用引号与可不用
inc:function(){ //方法
this.price+=1;
}
}
obj3.inc(); //调用方法
obj3.inc();
console.log(obj3.price); //访问属性,输出101
对象中可以包含的内容是数组、函数、对象、基本数据类型等,他们间还可以嵌套或混合出现,数组中可以有对象或函数,对象中可以有数组或函数。
//定义数组
var users = [{name: "jack"}, {
name: "lucy", //常量
hobby:["读书","上网","代码"], //数组
friend:{name:"mark",height:198,friends:{}}, //对象
show:function(){ //函数
console.log("大家好,我是"+this.name);
}
}];
//对象中的this是动态的,指向的是调用者
users[1].show();
运行后输出:大家好,我是lucy
(2)、通过new创建对象
对象字面量创建非常直接,但不能复用属性或方法,使用new运算符创建并初始化一个对象,new后面接一个构造函数(constructor),调用时默认返回this对象。new 关键字会进行如图3-1四个步骤的操作。
//使用内置构造函数创建对象
var obj1=new Object(); //创建一个空对象,等同{};
obj1.name="mark";
obj1.show=function () {
console.log("我叫"+this.name);
}
var arr1=new Array(); //创建一个数组对象,等同[]
obj1.show(); //调用obj1中的show方法
//使用自定义构造函数创建对象
function User(name){
this.name=name; //添加属性
this.show=function () { //添加方法
console.log("我叫"+this.name);
}
}
var rose=new User("rose"); //创建User对象
var jack={};
User.call(jack,"jack"); //借调构造函数User,完成对象的初始化
rose.show();
jack.show();
console.log(rose instanceof User); //rose是否为User类型的实例
console.log(rose instanceof Object); //rose是否为Object类型的实例
console.log(jack instanceof User); //jack是否为User类型的实例
console.log(jack instanceof Object); //jack是否为Object类型的实例
运行结果如图3-2所示。
图3-2 通过new创建对象示例运行结果
从输出结果中可以看出通过call方法借调构造器生成的对象依旧是Object类型,并不是User类型,这是因为jack这个对象是通过字面量直接创建的,call只是调用构造函数初始化了这个对象。
2.2、使用对象
访问对象主要包含取值、修改、删除、迭代操作。取值使用点运算最多,但遇到key为关键字或含有空格时也可以使用"对象名[key]"的方式完成,示例代码如下:
//定义对象字面量
var obj1={name:"foo",age:19,"nick name":"bar"};
//取值
console.log(obj1.name); //等同于obj1["name"],点运算取值
console.log(obj1["nick name"]); //因为key中含有空格这里只能如此访问,字符串索引取值
//修改
obj1.age=18;
//删除属性
delete obj1.name;
//枚举,将对象中的key逐个取出(无序)
for(var key in obj1){
console.log(key+"->"+obj1[key]);
}
运行结果如图3-3所示。
图3-3 访问对象示例运行结果
通过示例可以看出delete后对象中的成员就被删除了,迭代输出结果中并没有name,delete运算符只能删除自有的属性,不能删除继承的属性,删除成功后会返回是否删除成功的布尔类型值(true/false);另外要注意的是迭代默认是无序的,并不会按照对象中属性的顺序输出。
2.3、原型及关系
在JavaScript中关于原型(prototype)、原型链、__proto__、Function、Object等内容是较难理解的,因为它与经典的面向对象(如Java与C++中面向对象的概念)存在较大的差别。只有理解了这些概念与特性我们才能更好的掌握JavaScript的面向对象核心。
prototype(原型):函数中的一个属性,指向该构造函数的原型对象(原型对象用于实例共享属性和方法),任何函数都拥有该属性。
__proto__:对象中的一个非标准内部属性,指向构造函数的原型对象,在ECMA-262第五版中被称为[[prototype]],且没有标准的方式能访问到,__proto__为浏览器支持属性;作为对象的内部属性,是不能被直接访问的。为了方便查看一个对象的原型,Firefox和Chrome中提供了"__proto__"这个非标准的访问器(ECMA引入了标准对象原型访问器"Object.getPrototype(object)")。任何对象都拥有该属性。
constructor:原型对象中的一个属性,指向该原型对象的构造函数;
如图3-4所示我们先来看看函数、对象、原型、Object与Function关系之间的关系图。
图3-4 函数、对象、原型、Object与Function关系图
(1)、任何函数是对象也是构造器(constructor)。函数与构造器在定义上没有任何区别,习惯把构造器名称的首字母大写。
function Cat() {}
console.log(typeof Cat); //function
console.log(Cat instanceof Function); //true
console.log(Cat instanceof Object); //true
var mycat=new Cat(); //调用构造函数,创建对象
console.log(mycat instanceof Cat); //true
从输出结果可以看出Cat是一个函数也是一个对象,函数是一个特殊的对象,Function构造了所有函数(function),函数创建了对象,Function自己创建了自己。
(2)、任意函数都有prototype属性,其属性值将被作为原型赋值给所有对象实例(也就是设置实例的__proto__属性)。
function Cat() {} //定义函数
console.log(Cat.prototype); //输出函数的原型
function foo(){}; //定义函数
console.log(foo.prototype); //输出函数的原型
Cat.prototype={name:"foo"}; //修改函数的原型,默认为Object类型的对象
var mycat=new Cat(); //创建Cat类型的对象
console.log(mycat.name); //从原型链上获得name属性
运行结果如图3-5所示。
图3-5 访问对象示例运行结果
(3)、JavaScript中所有的对象(函数也是对象)都包含__proto__(原型)属性(非标准),都指向其构造器的prototype。
function Cat() {}; //构造函数
var o={}; //对象
var fun=new function(){}; //函数表达式
console.log(Cat.__proto__); //获得对象的原型(非标准)
console.log(o.__proto__);
console.log(fun.__proto__);
//获得对象的原型(标准)
console.log(Object.getPrototypeOf(fun)===fun.__proto__);
var mycat1=new Cat();
var mycat2=new Cat();
//对象的原型指向其构造器(函数)的原型对象
console.log(mycat1.__proto__===Cat.prototype);
//同一个构造器的对象共享原型
console.log(mycat2.__proto__===Cat.prototype);
运行结果如图3-6所示。
图3-6 所有对象都含有原型示例运行结果
Object.getPrototypeOf(对象)方法可以获得对象的原型,这是推荐的标准做法,从示例中可以看了任意对象都包含原型,同一个构造器的实例共享一个原型,可以达到继承的目的。
(4)、函数的__proto__都指向Function.prototype,它是一个空函数(Empty function)。
function Cat() {}; //构造函数
console.log(typeof Function.prototype); //function,注意这里不是Object
console.log(Function.prototype===Cat.__proto__); //true
function foo() {} //函数声明
console.log(Cat.__proto__===foo.__proto__); //true
函数都是Function的实例,所以函数的原型__proto__指向Function.prototype,这里非常特别的是它不是一个对象而是一个空函数。
(5)、函数都是由Function构造出来的,Function自己构造了自己,Object是由Function构造出来的,Function是构造器。
function Cat() {}; //构造函数
//函数都是由Function构造出来的
console.log(Cat instanceof Function); //true
//Function自己构造了自己
console.log(Function instanceof Function); //true
//Object是由Function构造出来的
console.log(Object instanceof Function); //true
(6)、对象的最终原型对象都指向了Object的prototype属性,Object的prototype对象的__proto__属性指向NULL。
function Cat() {}; //构造函数
var mycat=new Cat(); //构造Cat类型的对象mycat
console.log(mycat);
console.log(mycat.__proto__.__proto__===Object.prototype);
console.log(Object.prototype.__proto__);
运行结果如图3-7所示。
图3-7 原型链示例运行结果
从输出结果可以看出mycat是Cat构造出来的对象,所以mycat的__proto__指向Cat的prototype对象,Cat的prototype对象是Object构造出来的对象,所以Cat的原型__proto__指向Object的prototype对象,而Object的prototype对象的原型__proto__最终则指向null,到这里搜索资源就结束了,其实这就是原型链,JavaScript也就是通过该方式实现继承的,图3-8是用较直观的方式表现他们之间的关系。
图3-8对象的最终原型示例
(7)、所有prototype原型对象中的constructor属性都指向其构造器。
(8)、原型对象prototype中的成员是所有被创建对象共享的。
function Cat() {};//构造函数
Cat.prototype={name:"foo"}; //定义Cat的原型对象
var cat1=new Cat();
var cat2=new Cat();
console.log(cat1.name===cat2.name); //true
console.log(cat1.name,cat2.name); //foo foo
//修改构造函数中原型对象的name属性
Cat.prototype.name="bar";
//所有Cat的实例都被影响
console.log(cat1.name,cat2.name); //bar bar
示例中cat1与cat2都是Cat的实例,那么cat1与cat2的原型属性__proto__指向了Cat的prototype对象,该对象是cat1与cat2共享的,当修改原型对象时所有实例都受到影响。
这里需要注意的是如果我们在cat1中添加name属性如下所示:
cat1.name="min"; //cat1对象中添加属性name
console.log(cat1.name,cat2.name); //min bar
这里并不是访问原型对象中的name属性,而是向cat1中添加一个新的属性name,因为按就近原则cat1.name的优先级高于Cat.prototype中的name属性所以输出时选择了自己的属性name,可以通俗的理解cat1有两个name属性。
(9)、对象在查找成员时先找本对象本身的成员,然后查找构造器的原型中的成员,一步一步向上查找,最终查询Object的成员,这就是原型链。
使用prototype可以扩展内置对象,虽然JavaScript内置对象已非常强大,但面对复杂多变的开发需求肯定有不足的地方,这时可以通过修改prototype实现扩展功能。
这里扩展String构造函数,返回字符的长度,一个中文算2个长度。
//如果不存在则扩展
if (!String.prototype.lengthPro) {
//在String的原型中添加LengthPro函数
String.prototype.lengthPro = function () {
return this.replace(/[^\x00-\xff]/g, "**").length;
};
}
console.log("你好tom!".lengthPro()); //8
console.log("你好tom!".length); //6
lengthPro函数是通过正则将中文替换成2个字符串然后再调用内置的length达到目的的,这里的this就是字符串实例。
2.4、对象的成员
JavaScript中的对象的成员一般可以分成三类,分别是实例成员、原型成员与静态成员。
实例成员是对象自身的原生成员,不来自原型与原型链;静态成员属于构造器本身,调用时使用"构造器名称.成员名"的方式进行,使用该构造器创建的对象不会继承该成员;原型成员是所有被创建实例共享的,创建对象时自动继承给每一个对象。
function Cat() {
this.show=function () {
console.log("Cat的实例成员");
}
}
Cat.prototype.show=function(){
console.log("Cat的原型成员");
}
Cat.show=function () {
console.log("Cat的静态成员");
}
var cat=new Cat();
cat.show(); //Cat的实例成员
Cat.show(); //Cat的静态成员
delete cat.show; //删除实例成员show函数
cat.show(); //Cat的原型成员
从上面的代码可以看出当原型成员与实例成员冲突时实例本身的成员优先级要更高一些。
三、Object
-
Object是一个非常重要的内置函数对象,所有对象最终都源自Object,其他所有对象都继承 Object,都是Object的实例。调用Object构造函数可以创建新对象,Object原生方法分成两类:Object原型方法和Object静态函数。
3.1、调用Object构造函数
内置构造器Object使用new运算符可以创建新的对象,Object构造函数为给定值创建一个对象包装器,调用构造函数时如果参数是null或undefined,将返回一个空对象,否则,将返回一个与给定值对应类型的对象。
var obj1=new Object(); //{}
var obj2=new Object({name:"foo"}); //{name:"foo"}
var obj3=new Object(100); //相当于new Number(100)
虽然这样创建obj3是Number类型的一个实例,但这种方式不推荐,增加了理解代码的复杂度。
3.2、Object原型对象(Object.property)
JavaScript中一切对象都是Object类型的,所有的对象都从Object.property中继承方法和属性,当然新对象可以覆盖原型对象中的成员,通过Object的原型我们可以在每个实例中访问到的属性与方法如下:
(1)、Object.prototype.constructor属性
指向创建当前对象的构造函数。
(2)、Object.prototype.__proto__属性
指向当对象被实例化的时候,用作原型的对象。未标准化,也被标记为[[prototype]]。
(3)、Object.prototype.hasOwnProperty(属性名)方法
用于检查某个属性是否存在当前对象中,且此属性不是从原型链继承的。
function Cat() {this.name="foo";}
Cat.prototype={age:5}; //指定Cat的原型对象
var cat1=new Cat();
console.log(cat1.age); //5
//name是为cat1的自有属性
console.log(cat1.hasOwnProperty("name")); //true
console.log(cat1.hasOwnProperty("age")); //false,age是从原型链中获得
console.log(cat1.hasOwnProperty("nickname")); //false,不存在的属性
从输出结果可以看出只有存在且不是从原型链中获取的属性才返回真值。
(4)、Object.prototype.isPrototypeOf(对象)方法
用于测试当前对象的原型链上是否存在指定的原型,obj.isPrototypeOf(cat)则表示obj是否为cat的原型对象。
function Cat() {}
var obj={age:5};
Cat.prototype=obj; //指定Cat的原型对象
var cat=new Cat();
//obj是否为cat的原型
console.log(obj.isPrototypeOf(cat)); //true
//cat的原型链上存在Object.prototype
console.log(Object.prototype.isPrototypeOf(cat)); //true
//Object.prototype是否为Function的原型
console.log(Object.prototype.isPrototypeOf(Function)); //true
(5)、Object.prototype.propertyIsEnumerable(属性)方法
用于判断指定属性是否可用for-in枚举。
var obj={types:[1,2,3]};
console.log(obj.propertyIsEnumerable("types")); //true
console.log(window.propertyIsEnumerable("obj")); //true
因为obj没有指定为那个对象的属性,默认obj属于window对象。
(6)、Object.prototype.toLocaleString()方法
直接调用 toString()方法。
(7)、Object.prototype.toString()方法
返回对象的字符串表示。
(8)、Object.prototype.valueOf()方法
返回指定对象的原始值,通常与toString()返回的结果相同。
3.3、Object静态成员
Object的静态成员直接通过"Object.成员名称"的形式调用,ES5、ES6中新增加了不少新的成员
(1)、Object.assign()
通过复制一个或多个对象来创建一个新的对象,如果目标对象中存在则覆盖,如果不存在则添加。
var source={a:1,b:2};
var target={b:3,c:4};
//使用source与{c:5,d:6}对象扩展target对象,返回扩展后的新对象
var result=Object.assign(target,source,{c:5,d:6});
console.log(result); //{b: 2, c: 5, a: 1, d: 6}
console.log(target); //{b: 2, c: 5, a: 1, d: 6}
console.log(source); //{a: 1, b: 2}
(2)、Object.create()
使用指定的原型对象和属性创建一个新对象。
var person = {
name: "foo",
show: function () {
console.log("我的名字是" + this.name);
}
};
//以person为原型创建一个新对象student
//新对象的__proto__指向person
var student=Object.create(person);
student.name="bar";
student.show(); //我的名字是bar
console.log(student);
运行结果如图3-9所示。
图3-9 Object.create()示例运行结果
从输出结果可以看了新创建的对象student的原型引用了person。
(3)、Object.defineProperty()
给对象添加一个属性并指定该属性的配置。
(4)、Object.defineProperties()
给对象添加多个属性并分别指定它们的配置。
(5)、Object.entries()
返回一个给定对象自身可枚举属性的键值对数组,与for-in的区别是,for-in循环也枚举原型链中的属性,但entries不会。
var obj=Object.create({a:1,b:2});
obj.c=3;
obj.d=4;
var array=Object.entries(obj); //[["c",3],["d",4]]
for(var i in array){
console.log(array[i][0],array[i][1]);
}
最后输出的结果是:c 3,d 4。原型链中的属性a:1与b:2并未获得的原因是entries()方法不会获取原型链上的属性,而for-in是可以的。
(6)、Object.freeze()
冻结对象,不能删除、更改、添加任何属性,原型也不能被修改,否则在严格模式下会抛出异常,但在非严格模式下只会静默抛出异常。
"use strict" //使用严格模式
var obj={a:1};
Object.freeze(obj);
obj.a=2; //修改属性的值
运行结果如图3-10所示。
图3-10 Object.freeze()示例运行结果
对象obj被冻结后仍然修改其属性a的值在严格模式下抛出异常:不能访问只读属性a。
(7)、Object.getOwnPropertyDescriptor()
返回对象指定的属性配置。
(8)、Object.getOwnPropertyNames()
返回指定对象自身的所有属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。
var obj={a:1,b:2,c:3};
console.log(Object.getOwnPropertyNames(obj)); //["a", "b", "c"]
console.log(Object.getOwnPropertyNames(["1",true,obj]));
//["0", "1", "2", "length"]
(9)、Object.getOwnPropertySymbols()
返回一个包含了指定对象自身所有的符号属性的数组,功能与Object.getOwnPropertyNames()类似,可以将给定对象的所有符号属性作为Symbol数组获取。Symbol是ES6中新增内容,在后面的章节中会讲到。
(10)、Object.setPrototypeOf()
设置对象的原型([[Prototype]])属性,注意是对象不是函数,函数的原型可以直接通过prototype修改。
var obj={};
Object.setPrototypeOf(obj,{name:"foo"});
console.log(obj.name); //foo
(11)、Object.getPrototypeOf()
返回指定对象的原型对象,虽然通过__proto__也能获取对象的原型,但这并不是一个标准属性。
function Cat() {} //定义构造器Cat
var proto={name:"foo"};
Cat.prototype=proto; //指定构造器的原型为proto对象
console.log(Object.getPrototypeOf(new Cat())===proto); //true
console.log(Object.getPrototypeOf(Cat)===proto); //false
需要注意的是从上面的代码可以看出第2次输出结果是false是因为所有函数的原型对象都指向了Function.prototype,大家要区分[[prototype]]与prototype的不同。[[prototype]](__proto__)是对象的属性,prototype是函数的属性。
(12)、Object.preventExtensions()
防止对象的任何扩展,不允许添加属性,但其原型对象仍然可以添加属性,可以删除属性。
"use strict"
var obj1={};
Object.preventExtensions(obj1);
//错误:Uncaught TypeError: Cannot add property name, object is not extensible
obj1.name="bar";
只有在严格模式下才会显示抛出异常,非严格模式则是静默错误。
(13)、Object.seal()
密封一个对象,不能添加新属性,不可删除属性,属性不可配置,属性不可删除,属性的值仍然可以修改。
"use strict"
var obj1={age:18};
Object.seal(obj1);
delete obj1.age; //错误:Cannot delete property 'age' of #<Object>
obj1.name="foo"; //错误:Cannot add property name, object is not extensible
(14)、Object.is(value1, value2)
比较两个对象是否相同,与==和===有所不同,不作类型转换,+0与-0不相等。
(15)、Object.isExtensible()
判断对象是否可扩展,被禁止扩展、密封与冻结的对象不允许扩展。
//默认可扩展
var obj1={};
console.log(Object.isExtensible(obj1)); //true
//被禁止扩展的对象不可扩展
Object.preventExtensions(obj1);
console.log(Object.isExtensible(obj1)); //false
//密封对象不可扩展
var obj2 = Object.seal({});
console.log(Object.isExtensible(obj2)); //false
// 冻结对象不可扩展
var obj3 = Object.freeze({});
console.log(Object.isExtensible(obj3)); // false
(16)、Object.isFrozen()
判断对象是否已经冻结,使用Object.freeze()可以冻结对象。
(17)、Object.isSealed()
判断对象是否已经密封,使用Object.seal()可以密封对象。
(18)、Object.keys()
返回一个包含所有给定对象自身可枚举属性名称的数组。
(19)、Object.values()
返回给定对象自身可枚举值的数组。
四、封装
封装(encapsulation)是面向对象编程的重要特性之一,能隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读取和修改的访问级别。默认JavaScript因为没有模块、包、类与块级作用域,封装特性非常差,但通过封装可以减少代码的冗余,使代码看起来更优雅美观,所以实现封装非常必要。
4.1、封装对象
(1)、使用对象封装
JavaScript中最简单的方法是通过对象将属性与方法封装在一起对外仅暴露对象名作为访问接口。
先来看一段没有封装的代码一:
var name="小猫";
var color="蓝色";
function run() {
console.log(color+"的"+name+"在跑!");
}
run(); //蓝色的小猫在跑!
上面的代码虽然实现了简单的功能但对外暴露了3个成员(name,color与run),可以封装成一个对象,代码二如下:
var dog={
name:"小狗",
color:"绿色",
run:function() {
console.log(this.color+"的"+this.name+"在跑!");
}
};
dog.run(); //绿色的小狗在跑!
代码二对外只暴露了一个访问点就是dog,而且可以再添加更多的属性与方法,而代码一随着功能的增加对外暴露的成员会更多。
(2)、使用构造器封装
通过对象可以封装但没有复用性,重复的脚本会引起许多问题,使用构造器封装可以解决对象封装的不能复用的缺陷。
function Animal(name,color) {
this.name=name;
this.color=color;
}
Animal.prototype.run=function () { //在原型中添加run方法
console.log(this.color+"的"+this.name+"在跑!");
}
var cat=new Animal("小猫","蓝色");
var dog=new Animal("小狗","绿色");
cat.run(); //蓝色的小猫在跑!
dog.run(); //绿色的小狗在跑!
需要注意的是没有将run方法写在构造器中的原因是:函数也是对象,每次新创建对象时都要再创建函数对象,这样会降低性能,而将方法放在原型则被所有对象共享,只需创建一次即可。
使用构造器同样达到了封装的目的,当然他与对象的封装可以应用在不同的场景,两者并不矛盾,本质上构造器只是提供了一种创建对象的方法。
4.2、数据属性
数据属性包含一个数据值的位置,这个位置可以读取和写入值,直接在对象中定义的属性就是数据属性。
var dog={color:"白色"}; //数据属性
//{value: "白色", writable: true, enumerable: true, configurable: true}
console.log(Object.getOwnPropertyDescriptor(dog,"color"));
Object.getOwnPropertyDescriptor用于获得属性的描述对象,从输出结果可以看出直接定义的属性的默认配置值。
ES5中通过Object.defineProperty()方法可以定义属性,可以通过参数配置属性的特征,方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回该对象。语法如下:
Object.defineProperty(要定义属性的对象,属性名称,属性的特征描述对象)
属性描述对象中的4个参数:
configurable:是否允许通过delete删除属性,能否修改属性的特性,是否允许把属性修改为访问器属性,默认为false。
enumerable:是否可枚举,也就是能否通过for-in循环返回属性,默认为false。
value:该属性对应的值,默认为 undefined。
writable:是否允许修改属性的值。默认为 false。
var dog={color:"白色"}; //普通数据属性
Object.defineProperty(dog,"name",{ //定义带描述对象的数据属性
writable:false, //只读
value:"小狗", //默认值
enumerable:false, //不可枚举(遍历时不出现)
configurable:false //不可重新配置
});
dog.name="狗狗"; //修改属性name的值
delete dog.name; //删除属性
console.log(dog.name);
Object.defineProperty(dog,"name",{ //重新配置,报错
writable:true
});
运行结果如图3-11所示。
图3-11 数据属性示例运行结果
从输出结果可以看出因为不可写所以输出的值还是初始值,如果在严格格式下将直接抛出错误(Cannot assign to read only property 'name' of object '#<Object>');因为描述对象不允许再配置所以再配置时抛出了错误。
4.3、访问器属性
访问器属性与数据属性不一样,它不包含数据值,使用getter与setter函数,getter实现读,setter实现写,访问器属性不能直接定义,需要使用Object.defineProperty()静态函数,语法格式与数据属性的一样,但描述对象有些区别,对应的4个特性如下:
configurable:是否允许通过delete删除属性,能否修改属性的特性,是否允许把属性修改为访问器属性,默认为false。
enumerable:是否可枚举,也就是能否通过for-in循环返回属性,默认为false。
get:读取属性值时调用的函数,默认是undefined。
set:写入属性值时调用的函数,默认是undefined。
var cat={name:"小猫",_age:1};
//定义访问器属性age
Object.defineProperty(cat,"age",{
configurable:true, //可再配置
enumerable:true, //可枚举
get:function () { //读
return this._age;
},
set:function (value) { //写
if(!isNaN(value)&&value>0) { //约束写入值
this._age = value;
}else{
throw {message:"年龄必须是大于0的数字!"};
}
}
});
console.log(cat.age);
cat.age=-1; //写入非法值
运行结果如图3-12所示。
图3-12 访问器属性示例运行结果
从输出结果可以看出当访问cat.age时间接的调用了数据属性_age,当写入的值不满足属性要求时抛出了异常。使用Object.defineProperties()时可以一次定义多个数据属性与访问器属性。
4.4、使用闭包封装属性
访问器属性中的_age只是基于一种规则的约定,视下划线开始的成员为私有成员,实际上可以任意的修改_age的值,可见它并不是真正意见上的私有成员,另外ES5并非所有的浏览器都支持,使用闭包可以封装属性。
var cat=(function () {
var _age=1;
return {
name:"小猫",
getAge:function () {
return _age;
},
setAge:function (value) {
if(!isNaN(value)&&value>0) { //约束写入值
_age = value; //注意这里没有this,因为访问的是外部函数的值
}else{
throw {message:"年龄必须是大于0的数字!"};
}
}
}
})(); //IIFE
console.log(cat.age); //直接访问
cat.setAge(18); //写
console.log(cat.getAge()); //读
cat.setAge(-1); //非法值
运行结果如图3-13所示。
图3-13 闭包封装属性示例运行结果
小贴士:关于IIFE、闭包与作用域的内容在上一章已经讲到,可以参考上一章的内容。
五、继承
继承(inherit)是面向对象编程的一个重要特性,继承能提高复用性,一般通过接口继承或实现继承,JavaScript无接口也无接口继承但可以通过原型实现继承。在ES2015/ES6中引入了class关键字,但那只是语法糖,JavaScript仍然是基于原型的继承。
5.1、借调父构造函数实现属性继承
使用函数的call方法可以动态的修改this的指向,为了初始化子类中的属性可以通过在子类构造器中借调父构造器完成属性的初始化,达到继承的目的,代码如下:
//父类,动物 function Animal(name){ this.name=name; } Animal.prototype.show=function(){ //父类原型中的展示方法 console.log("这是一只名为:"+this.name+"的狗"); } //子类,狗 function Dog(name,color){ //借调父类构造方法用于继承属性,初始化属性值 Animal.call(this,name); this.color=color; } var dog=new Dog("泰迪","白色"); console.log(dog.name,dog.color); console.log(dog.show);
运行结果:
从控制台输出结果可以看到属性name与color都继承成功了,但是原型中的show方法并没有被继承成功。
5.2、继承父类原型中的函数
每个函数都有原型属性prototype,prototype属性指向构造函数的原型对象,当调用构造器创建新对象时会在新对象中添加__proto__属性([[prototype]])指向构造器的原型对象,所有的实例共享该原型,新创建的对象会中原型中获得新的成员,从而达到继承与复用的目的。
上面示例中的Dog的原型还是指向一个类型为Object的对象,并不能实现对父类原型中的对象继承,但是直接将子父类的原型指向父类的原型对象又会引起子类修改原型时影响父类的问题,这里的处理方法是将子类的原型指向一个父类的实例对象,间接的继承父类原型中的成员。
//父类,动物 function Animal(name){ this.name=name; } Animal.prototype.show=function(){ //父类原型中的展示方法 console.log("这是一只名为:"+this.name+"的狗"); } //子类,狗 function Dog(name,color){ //借调父类构造方法用于继承属性,初始化属性值 Animal.call(this,name); this.color=color; } //子类的原型指向一个新的父类实例,因为name初始化过,这里不再指定参数 Dog.prototype=new Animal(); var dog=new Dog("泰迪","白色"); console.log(dog.name,dog.color); dog.show(); console.dir(dog); console.log(Dog.prototype.constructor);
运行结果如图3-14所示。
图3-14 使用原型实现继承示例运行结果
5.3、修改原型对象中构造器的指向
从上面的示例中可以看出子类的原型对象中的构造器指向是错误的,依然指向Animal构造器,应该修改其指向,代码如下:
//父类,动物 function Animal(name){ this.name=name; } Animal.prototype.show=function(){ //父类原型中的展示方法 console.log("这是一只名为:"+this.name+"的狗"); } //子类,狗 function Dog(name,color){ //借调父类构造方法用于继承属性,初始化属性值 Animal.call(this,name); this.color=color; } //子类的原型指向一个新的父类实例,因为name初始化过,这里不再指定参数 Dog.prototype=new Animal(); //修改Dog子类原型的构造器指向Dog而非Animal Dog.prototype.constructor=Dog; var dog=new Dog("泰迪","白色"); console.log(dog.name,dog.color); dog.show(); console.dir(dog); console.log(Dog.prototype.constructor);
输出结果:
5.4、原型链
当我们访问一个对象的成员时会首先查找对象自身,如果不存在时将查找__proto__所指向的原型对象,因为所有的对象都拥有__proto__属性,所以将一直向上查找,直到查找到Object.prototype对象的__proto__属性,它是指向null,这时就结束了,这就是原型链。原型链是JavaScript中实现继承的核心。
function Animal() {this.name="动物";} //动物
function Dog() {this.color="白色";} //狗
Dog.prototype=new Animal(); //继承动物
function Poodle() {this.weight="5kg";} //贵宾犬
Poodle.prototype=new Dog(); //继承狗
var poodle=new Poodle();
console.log(poodle.name,poodle.color,poodle.weight);
console.log(poodle instanceof Poodle);
console.log(poodle instanceof Dog);
console.log(poodle instanceof Animal);
console.log(poodle instanceof Object);
运行结果如图3-16所示。
图3-16 原型链示例运行结果
对象与原型,原型与原型之间的关系如图3-17所示。
图3-17 通过原型链实现继承
从输出的结果可以看出poodle对象同时是Poodle、Dog、Animal与Object类型。
JavaScript中的继承因为没有统一的标准,所以根据需要出现了许多不同的形式,这里仅讲了最基本的继承方式,还有借用构造函数、组合继承、原型式继承、寄生式继承和寄生组合式继承、Object.create()等方式。
六、多态
多态(Polymorphism)是指同一个接口,使用不同的实例而执行不同操作,是面向对象重要特性,多态性一般表现为重写与重载。
6.1、覆盖(Override)
JavaScript没有接口,但支持重写功能,根据原型链中查找成员的规则自身成员的优先级高于原型链中成员的优先级,遵照就近原则。
function Animal() {}
Animal.prototype={ //指定动物类型的原型对象
constructor:Animal,
eat:function () {
console.log("动物在吃东西");
}
};
function Cat() {}
Cat.prototype=new Animal();
Cat.prototype.eat = function () { //覆盖原型对象中的eat方法
Animal.prototype.eat.call(this); //调用父类中的eat方法
console.log("猫在吃小鱼");
}
new Cat().eat(); //动物在吃东西 猫在吃小鱼
Cat.prototype.eat严格意义上来说并不是重写了Animal中的eat方法而是在自己的原型对象中添加了一个优先级更高的eat方法,使和call或apply可以调用父类中的方法且可以指定执行上下文为当前对象。
JavaScript是一种弱类型的动态语言,对象类型可以任意转换,这意味着JavaScript对象的多态性是与生俱来的,它在编译时没有类型检查的过程,既没有检查创建的对象类型,又没有检查传递的参数类型。
function Cat() {}
Cat.prototype.eat = function () {
console.log("猫在吃小鱼");
}
function Dog() {}
Dog.prototype.eat = function () {
console.log("狗在吃骨头");
}
function animalEat(animal) {
if(animal.eat instanceof Function){
animal.eat();
}
}
animalEat(new Cat()); //猫在吃小鱼
animalEat(new Dog()); //狗在吃骨头
animalEat(new Object()); //无输出
上面这段代码就达到了多态的目的,但因为没有接口的约束与语法检查eat这个函数在调用前作了判断。
6.2、重载(Overload)
面向对象中同名方法不同参数满足不同的功能需要就是重载,重载增加了灵活性。JavaScript是弱类型语言,没有重载,但可以模拟实现。
//定义实现重载的加法方法
function add() {
//单个数字时加1
if(arguments.length==1&&typeof arguments[0]==="number"){
return arguments[0]++;
}else{
//多个数字时累加
var sum=0;
for(var i=0;i<arguments.length;i++){
if(typeof arguments[i]==="number"){
sum+=arguments[i];
}
}
return sum;
}
}
console.log(add(100)); //101
console.log(add(100,200)); //300
console.log(add(100,200,500)); //800
尽管JavaScript没有真正的重载,但是重载的达到的效果在JavaScript中却十分常见,比如Array的splice( )方法,本质都是对参数的个数与类型判断,来决定执行什么操作。
七、JSON
7.1、JSON概要
JSON(JavaScript Object Notation,即JavaScript对象表示法)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于设备解析和生成。JSON采用完全独立于语言的文本格式,包含Java与C#在内的多数程序设计语言都支持JSON。JSON慢慢在取代笨重的XML。
JSON有两种结构:"键/值对"与"数组"。前者可以理解为对象、字典与结构等表现形式如下:
{key:value,key:value,…}
后者可以理解为序列或集合,表现形式如下:
[number,boolean,string,null,array,object,...]
JavaScript不是JSON,JSON也不是JavaScript,JavaScript中的对象表示与JSON非常类似但也有些区别:
1、属性名必须用双引号括起来;最后一个属性后不能有逗号。{age:18,}这样写在JavaScript中是正确的,但JSON中需要修改为:{"age":18}。
2、JSON不支持undefined与变量。因为undefined是JavaScript中特殊存在的,变量需要运算才可以获得结果。
3、数值不能出现前置零;小数点,后至少有一位数字。{"price":03,"size":1.}在JavaScript中是正确的,在JSON中就需要修改成{"price":3,"size":1.0}。
4、字符串只能是Unicode编码。
5、没有末尾分号,即最后一相大括号后面不要加分号。
将语言中特定的对象转换成字符串或其它便于交换的格式称为序列化,反过来将字符串或特定格式转换成语言中的对象称为反序列化,作为一种数据交换格式这非常重要,这里只讲解JavaScript中的序列化与反序列化。
7.2、序列化
JavaScript中将对象转换成JSON字符串称为序列化JSON,通常会使用全局对象JSON,部分浏览器中并没有内置该对象,需要引入或Polyfill,JSON.stringify()可能将一个JavaScript对象或者数组转换为一个JSON字符串,语法格式如下:
JSON.stringify(value[, replacer [, space]])
value:要序列化的对象。
//产品对象
var product = {
"id": 10001, "name": "手机", "price": 1937.5,
size: { 700, height: 1300}
};
//将JavaScript对象序列化成JSON字符串
var json = JSON.stringify(product);
console.log(json);
输出:{"id":10001,"name":"手机","price":1937.5,"size":{"width":700,"height":1300}}
Replacer:过滤(可选)
(1)、当该参数是一个函数时,被序列化的值的每个属性都会经过该函数的转换和处理;
//将JavaScript对象序列化成JSON字符串,并提升价格与替换名称中的关键字
var json = JSON.stringify(product,function (key,value) { //过滤函数
switch (key) {
case "price": //如果键的值是price
return value*1.1;
case "name":
return value.replace(/手/igm,'耳'); //将手替换成耳,关键词过滤
default:
return value;
}
});
console.log(json);
输出:{"id":10001,"name":"耳机","price":2131.25,"size":{"width":700,"height":1300}}
从输出结果中可以看出name对应的值被替换了,价格被提高了10%,起到了过滤的作用,但如果对象与过滤函数较复杂,需注意性能问题。
(2)、当该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的JSON字符串中。
//将JavaScript对象序列化成JSON字符串,只需要name与price属性
var json = JSON.stringify(product,["name","price"]);
console.log(json);
输出:{"name":"手机","price":1937.5}
space:缩进字符,美化输出效果(可选)
(1)、如果设置数字表示空格个数;最大为10,默认无空格。
//将JavaScript对象序列化成JSON字符串,美化输出结果
var json = JSON.stringify(product,null,2);
console.log(json);
输出:
{
"id": 10001,
"name": "手机",
"price": 1937.5,
"size": {
"width": 700,
"height": 1300
}
}
(2)、如果设置参数为字符串则以字符串作为缩进字符。
//将JavaScript对象序列化成JSON字符串,指定缩进字符
var json = JSON.stringify(product,null,"+++");
console.log(json);
输出:
{
+++"id": 10001,
+++"name": "手机",
+++"price": 1937.5,
+++"size": {
++++++"width": 700,
++++++"height": 1300
+++}
}
在对象中定义toJSON函数也可以实现自定义序列化的需求,Date对象就定义了toJSON函数会将日期自动转换成ISO 8601日期字符串。各种开发语言对后台对象的序列化JSON的支持都非常完善了,当然也可以使用许多优秀的三方开源库。
7.3、反序列化
JSON反序列化是将JSON字符串解析成JavaScript对象。eval()函数因为存在安全风险已不再建议使用,JSON.parse可以完成该功能,其语法格式如下:
JSON.parse(text[, reviver])
text:要转换的JSON字符串
//JSON字符串
var json = '{"id": 10001, "name": "手机", "price": 1937.5}';
//解析json字符串为JavaScript对象
var product=JSON.parse(json);
//访问对象中的成员
console.log(product.id+","+product.name+","+product.price);
输出:10001,手机,1937.5
reviver:还原函数(可选)。
如果reviver返回 undefined,则当前属性会从所属对象中删除,如果返回了其他值,则返回的值会成为当前属性新的属性值
//JSON字符串
var json = '{"id": 10001, "name": "手机", "price": 1000}';
//解析json字符串为JavaScript对象
var product=JSON.parse(json,function (key,value) {
if(key==="id"){
return undefined; //忽视id号
}
else if(key==="price"){
return value+350; //修改价格值
}
return value; //其它不变
});
//输出:{"name":"手机","price":1350}
console.log(product);
从上面的输出结果可以看出id属性被过滤掉了,价格(price)值被修改,名称(name)没有影响。解析时要注意日期格式的问题,从服务端返回的日期可能是一个unix时间戳,可以在还原函数中转换。
八、上机部分
8.1、上机任务一(30分钟内完成)
上机目的
1、掌握创建对象的方法。
2、掌握对象的访问操作。
上机要求
1、使用3种以上不同的方式创建一个学生对象,属性与方法定义如表3-1所示,其中家庭地址是一个子对象可以分解为(省、市、县/区),print方法用于向控制台输出学生的基本属性。
序号 |
类别 |
中文名称 |
英文名称 |
类型 |
备注 |
1 |
属性 |
学号 |
no |
Number |
100001-999999 |
2 |
姓名 |
name |
String |
||
3 |
生日 |
birthday |
Date |
出生年月日 |
|
4 |
是否在读 |
inSchool |
Boolean |
||
5 |
爱好 |
hobby |
Array |
["阅读","电影","足球"] |
|
6 |
家庭地址 |
family address |
Object |
{省市县},键必须带空格 |
|
7 |
方法 |
打印 |
|
Function |
显示所有属性 |
表3-1 学生对象的属性与方法
2、对创建的对象实现取值、修改、删除、迭代与方法调用操作。
推荐实现步骤
步骤1:先用对象字面量创建创建对象、访问对象,再使用其它方式创建对象。
步骤2:反复测试运行效果,优化代码,关键位置书写注释,必要位置进行异常处理。
8.2、上机任务二(50分钟内完成)
上机目的
1、掌握"数据属性"与"访问器属性"的定义与封装。
2、理解原型的作用。
2、掌握JSON的序列化与反序列化。
上机要求
1、升级上机任务一,请定义一个Student构造器,指定Student原型对象,要求原型中包含如下属性与方法,具体要求如表3-2所示(表3-2中未注明的内容默认与表3-1相同)。
序号 |
类别 |
中文名称 |
可配置 |
可枚举 |
默认值 |
是否可写 |
约束 |
1 |
属性 |
学号 |
false |
true |
101 |
false |
101-999间的数字 |
2 |
姓名 |
true |
true |
匿名 |
true |
2-4位中文 |
|
3 |
生日 |
true |
false |
1970-01-01 |
true |
不能超过当天 |
|
4 |
是否在读 |
true |
true |
true |
true |
||
5 |
爱好 |
true |
true |
[] |
true |
最多5个 |
|
6 |
家庭地址 |
true |
true |
{} |
true |
省市县不允许为空 |
|
7 |
方法 |
打印 |
表3-2 原型中的属性要求
2、调用构造函数实例化一个学生对象,设置每一个属性值,测试属性配置是否正确。
3、对创建的对象实现取值、修改、删除、迭代与方法调用操作。
4、请同时使用"数据属性"与"访问器属性"定义学生的原型对象,方法不变。
5、将创建的对象序列化成JSON字符串,要求将爱好合并成一个字符串,用逗号分隔开;生日显示为:yyyy-MM-dd格式;使用3个空格缩进;输出结果到控制台。
6、再将序列化后的JSON字符串反序列化成JavaScript对象,增加print方法,调用方法显示所有的属性值。
推荐实现步骤
步骤1:定义Student构造器,定义一个原型对象,在原型对象中定义数据属性,根据表格的要求设置每个属性的描述信息。
步骤2:调用构造函数创建对象,测试对象的使用。
步骤3:重新指定原型对象,使用"访问器属性"设置每个属性的描述信息,重复步骤2。
步骤4:完成序列化与反序列化功能。
步骤5:反复测试运行效果,优化代码,关键位置书写注释,必要位置进行异常处理。
8.3、上机任务三(20分钟内完成)
上机目的
1、理解prototype对象。
2、掌握扩展内置对象的方法。
3、了解JavaScript单元测试与测试框架。
上机要求
- 编写3个扩展方法,增强Date与Number内置对象,补充下面的代码。
-
weekday方法获得日期对象的星期,unixTimestamp将日期转换成Unix时间戳,toDate方法将Unix时间戳(Unix timestamp)转换成日期。
/**1、扩展Date对象,添加weekday方法*/
/**2、扩展Date对象,添加unixTimestamp方法*/
/**3、扩展Number对象,添加toDate方法*/
var date=new Date(2031,11,29,12,59,35);//创建一个日期对象,指定年月日时分秒
//获得date的星期值
console.log(date.weekday()); //输出:星期一
//将日期转换成Unix时间戳(Unix timestamp)
console.log(date.unixTimestamp()); //输出:1956286775
//将Unix时间戳(Unix timestamp)转换成日期
console.log(new Number(1956286775).toDate().toLocaleString());
//输出:2031/12/29 下午12:59:35
- 使用Mocha、Jest、Chai等测试框架完成单元测试。(选作)
提示:Unix时间戳(Unix timestamp),或称Unix时间(Unix time)、POSIX时间(POSIX time),是一种时间表示方式,定义为从格林威治时间1970年01月01日00时00分00秒起至现在的总秒数。Unix时间戳不仅被使用在Unix系统、类Unix系统中,也在许多其他操作系统中被广泛采用。
Date类型的getTime()方法可返回距1970年1月1日之间的毫秒数。new Date(毫秒)构造函数是支持使用毫秒创建日期对象。
推荐实现步骤
步骤1:创建脚本文件,依次扩展3个方法并测试是否达到预期效果。
步骤2:反复测试运行效果,优化代码,关键位置书写注释,必要位置进行异常处理。
8.4、上机任务四(50分钟内完成)
上机目的
1、掌握基本的继承方法。
2、理解多态。
3、了解Canvas绘画技术。
上机要求
- 如图3-18所示创建3个构造函数,定义好属性与方法,draw方法向控制台输出当前形状的位置,area方法计算形状的面积。构造方法要求可以初始化所有参数。
图3-18 继承关系
2、实现形状间的继承关系,如图3-18所示。
3、分别创建不同类型的测试对象,定义对象时传入参数,调用对象中的方法。
4、重写draw方法,通过Canvas实现绘图功能,参考代码如下所示:
<canvas id="canvas1" width="300" height="150"></canvas>
<script>
var c=document.getElementById("canvas1");
var cxt=c.getContext("2d");
cxt.fillStyle="#FF0000";
cxt.fillRect(100,100,100,50);
cxt.beginPath();
cxt.arc(50,50,40,0,Math.PI*2,true);
cxt.closePath();
cxt.fillStyle="#0000FF";
cxt.fill();
</script>
图3-19 Canvas绘图参考示例
5、定义一个drawHandler方法,接受不同的形状实例,调用绘图方法,在页面上绘出不同的图形,请使用多态的方式。
6、参照图3-4与图3-15画出对象、函数、原型、Function与Object间的关系图。(选作)
推荐实现步骤
步骤1:创建页面,按要求定义好三个构造方法,并实现其继承关系,测试效果。
步骤2:学会HTML5中使用Canvas绘画的基本技巧后,重写draw方法。
步骤3:反复测试运行效果,优化代码,关键位置书写注释,必要位置进行异常处理。
8.5、代码题
1、定义一个对象实现深拷贝,即克隆一个完全独立的对象,有多种方式可以实现,试比较他们之间的区别。
2、扩展Date对象,实现日期格式化功能,效果如下所示:
var date = new Date();
date.formate("yyyy-MM-dd hh:mm:ss") //输出:2032-01-15 23:59:59