一、概述
面向對象程式程式設計(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: {width: 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、定義一個對象實作深拷貝,即克隆一個完全獨立的對象,有多種方式可以實作,試比較他們之間的差別。
九、源代碼
https://gitee.com/zhangguo5/JS_ES6Demos.git
十、教學視訊
https://www.bilibili.com/video/BV1bY411u7ky?share_source=copy_web