前言
在Es6之前,由于javascript沒有對類的支持,也就是說它并不具備如傳統后臺語言(比如java)擁有類的功能,所謂類就是用來描述事物中的屬性和行為的,類的特征是由成員組成的,而屬性對應的就是類中的成員變量,而方法對應的就是類中的成員方法,這是傳統oop語言的描述,然而在javascript中,雖沒有類的概念,但是它往往是通過構造函數和原型對象來給對象模擬與類相似的功能,但是這些相似的功能并不一定表現的與類完全一致,其實創建構造函數的過程,就是創建模板的過程,類一定程度上與此相似,創建多個共享的特定的屬性和方法,用于生成對象的餅干工具,主要目的是提高代碼的可復用性,也提高了代碼的性能,有時候,在我們無意間就已經在使用了這些特性,什么構造函數,原型,個人覺得,初次理解起來很是抽象,也稀里糊涂的覺得實際開發中到底有什么卵用,也許后者在不涉及復雜的功能需求時,平時用得不多,顯然Es6中已新增了類class的功能,越來越嚴格,越來越像后端語言,Es6,Es7,Es8新增的諸多方法也越來越強大,標準也越來越完善,但是我覺得理解構造函數與原型對象是必須的,是js面向對象編程的基礎,今天就我的學習和使用跟大家分享一下學習心得
正文從這里開始~
什么是函數
先看下面一簡易代碼
var funA = function(){
console.log("我是匿名函數保存在變量funA中");
}
var funB = [function(){
console.log("我是匿名函數保存在數組funB中");
}]
var funC = {
method:function(){
console.log("我是匿名函數保存在對象funC中");
}
}
// 函數的調用
funA(); // 普通函數的調用
funB[0](); // 函數存入數組中的調用
funC.method(); // 對象調用方法的使用
// 函數可以作為參數傳遞,也可以作為返回值返回
var funD = function(funParm){
return funParm;
}
var runFunParmPassedToFunD = funD(function(){
console.log("歡迎關注微信itclanCoder公眾號");
})
runFunParmPassedToFunD();
// 函數是對象,也就是說函數也擁有屬性
var FunE = function(){}
FunE.property = "隨筆川跡";
console.log(FunE.property);
// 證明函數式對象
console.log("funA的數據類型是",typeof funA);
console.log("funA具體所屬",Object.prototype.toString.call(funA));
console.log("funA是由Object的一個實例對象?",funA instanceof Object);
console.log("funA函數下面的構造器是",funA.constructor);
console.log("funA函數是由Object構造出來的?",funA.constructor == Object); // false
console.log("funA下面的原型",funA.prototype); // funA下面的原型
console.log("Object下的原型",Object.prototype); // Object對象下原型
console.log("funA原型下構造器",funA.prototype.constructor);//function fun(){}
console.log("對象原型下的構造器",Object.prototype.constructor);
控制臺輸出結果如下:
結論
:
- 函數也是功能代碼塊,一個封閉區間短小的腳本,如果多次使用同一段代碼,就可以把它封裝成一個函數,允許在代碼里隨調隨用,利用函數封裝來避免重復鍵入大量相同的內容,不過函數的真正威力在于傳參的能力,可以把不同的數據傳遞給它們,使用這些數據去完成預定的操作
- 函數是一等公民,是對象,是值,可以存儲在一個變量,數組或者對象中
- 函數可以傳遞給函數,并由函數返回,函數擁有屬性
- 函數總有返回值(換句話說就是有return語句,當然構造器函數除外,因為它默認會返回構造器函數調用,當構造函數的調用執行時,會顯示的返回返回)
什么是構造函數
定義:構造函數就是你用new
關鍵字創建對象時調用的函數
作用(優點):創建多個共享特定屬性和行為的對象,主要是用于生成對象的餅干模具
缺點:當實例化多個對象時,會重復的創建對象,造成內存空間的浪費,增大CPU的開銷,并沒有消除代碼的冗余,(如后面代碼所示,原型正好解決了此類問題)
// 聲明一構造函數,首字母大寫
function Animal(name,age){
// this == new Animal();new會自動的創建this對象,且類型就是該構造安徽省農戶的類型,構造函數不需要返回值,因為new會顯示的返回,return的值就等于函數名+()的調用
this.name = name; // 自定義屬性
this.age = age; // 同上
this.fun = function(){ // 自定義方法
return this.name+" "+this.age+"歲了";
}
}
// 實例化對象
var animal1 = new Animal("cat",2);
var animal2 = new Animal("dog",3); console.log(animal1.name,animal1.age,animal2.name,animal2.age); // cat 2 dog 3
console.log(animal1.fun(),animal2.fun()); // cat 2歲了 dog 3歲了
console.log(animal1.hasOwnProperty("name"));
console.log(animal1.hasOwnProperty("age"));
console.log(animal1 instanceof Animal); // true,證明animal1是Animal1是Animal構造函數創建出來的
console.log(animal2 instanceof Animal);
console.log(animal1.constructor === Animal); // true
console.log(animal2.constructor === Animal); // true
console.log(animal1.fun == animal2.fun); // false
示例代碼截圖如下
問題:同一個構造函數創建出來不同的實例化對象,公用的方法不等同,也就是說,當你new一個構造器對象,上面的構造函數就執行一遍,每次都會新建一個function,會新開辟一個內存空間,每次都是指向一個新的堆的對象 ,這樣占用內存消耗非常的大,怎么解決這個問題
解決辦法1
:將構造函數里面自定義的方法拿出來,獨立放在構造函數外如下示例代碼所示
// 聲明一構造函數,首字母大寫
function Animal(name,age){
this.name = name; // 自定義屬性
this.age = age; // 同上
this.fun = fun;
}
// 把構造函數里面自定義的方法拿出來
function fun(){
return this.name+" "+this.age+"歲了";
}
// 實例化對象
var animal1 = new Animal("cat",2);
var animal2 = new Animal("dog",3);
console.log(animal1.fun === animal2.fun); // true
控制臺截圖如下所示
解決辦法2
利用原型正好解決實例化多個對象時,避免構造函數內的方法重復創建(如后面的示例代碼所示)
普通函數與構造函數的區別
- 有new與無new的差別
- 寫法上,構造函數首字母大寫(目的只是用于區分普通函數與構造函數,提醒你在創建實例化對象前加new操作符)
- 當函數沒有被new調用時,構造函數中的this就能與全局this對象(即window)
示例代碼如下所示:
// 聲明函數
function Animal(name,age){
this.name = name;
this.age = age;
this.fun = function(){
return this.name+" "+this.age+"歲了";
}
//console.log(this); window
}
// 無new的情況
var animal1 = Animal("cat",2);
var animal2 = Animal("dog",3);
console.log(animal1 instanceof Animal); // false
console.log(animal2 instanceof Animal); // false
console.log(Object.prototype.toString.call(animal1));//[object Undefined]
console.log(Object.prototype.toString.call(animal2));
console.log(name,age); // dog 3
console.log(animal1.name,animal1.age); //報錯
控制臺輸出結果
從上面的代碼中可以看出,當一個函數無new關鍵字的調用時,構造函數中的this對象指向的是全局對象window,所以構造函數式依靠new提供返回值,上面的類型檢測,值為undefined,正是如此,沒有使用new,則為普通函數,只不過是一個沒有返回值的語句函數,對this賦值屬性和方法,相當于在全局下添加屬性和方法,如果加了use strict,在嚴格模式下,還會報,嚴格模式下,并沒有全局對象設置this,返回的是undefined
針對以上問題,如果想普通函數也具有構造函數的功能,怎么做?如下代碼所示
// 聲明構造函數
function Animal(name,age){
// 加一this條件判斷,用instanceof來檢查自己是否被new調用
if(this instanceof Animal){
this.name = name;
this.age = age;
this.fun = function(){
return this.name+" "+this.age+"歲了"; }
}else{
// 以new遞歸調用自己來為對象創建正確的實例,這樣做的目的是在不同的情況下表現出一致的行為,常常是為了保護那些忘記了使用new的情況
return new Animal(name,age);
}
}
// 無new的情況
var animal1 = new Animal("cat",2);
var animal2 = Animal("dog",3);
console.log(animal1 instanceof Animal); // true
console.log(animal2 instanceof Animal); // true
console.log(Object.prototype.toString.call(animal1));//[object object]
console.log(Object.prototype.toString.call(animal2));//[object object]
console.log(animal1.name,animal1.age);
console.log(animal2.name,animal2.age);
控制臺輸出結果如下
為何內置構造函數無new也能工作
示例代碼如下所示
var arr = Array; // 當沒有參數時,構造函數后面的圓括號可以省略
var obj = Object({
name:"隨筆川跡",
sex:"boy",
fun:function(){
return this.name+" "+this.sex+" "+Object.prototype.toString.call(this);
}});
console.log(obj.fun());
截圖如下所示
原因
:因為那些內置系統構造函數(Array,Object,RegExp,Date,Error,String等)都被設計為作用域安全的構造函數,也就是說在整個全局范圍內都是可見的,一個作用域安全的構造函數無new也可以工作,并返回同樣類型的對象
原型對象
protype
:
- 作用1:去改寫對象下面公用的方法或者屬性,讓公用方法或者屬性在內存中存在一份(也就是更改構造器函數底下屬性和方法,解決了不會重復創建構造的過程,目的是提高性能),可以看作是對象的基類
- 作用二:在原有的對象基礎上上,通過prototype進行額外的,封裝,拓展(如后面示例代碼)
- 原型是基于構造函數的(也就是說原型是掛載在構造函數下的,先有構造函數,才會有原型)
如下原型示例代碼
function ProtoFun(width,height){
this.width = width;
this.height = height;
this.method = function(){
return "我是構造函數下自定義的方法"
}
}
// 構造函數.原型下添加屬性
ProtoFun.prototype.color = "red";
// 構造函數.原型下添加方法
ProtoFun.prototype.fun = function(){
return this.width+" "+this.height+" "+this.color;
}
// 上面兩個通常可以合并成下面一個
ProtoFun.prototype.init = {
color:"red",
fun:function(){
return this.width+" "+this.height+" "+this.color;
}
}
var elemObj1 = new ProtoFun(100,100);
var elemObj2 = new ProtoFun(200,200);
console.log(elemObj1.width,elemObj1.height); // 100 100
console.log(elemObj2.width,elemObj2.height); // 200 200
console.log(elemObj1.color,elemObj1.fun(),elemObj1.init.color,elemObj1.init.fun());
console.log(elemObj2.color,elemObj2.fun(),elemObj2.init.color,elemObj2.init.fun());
console.log(elemObj1.method===elemObj2.method); // false
console.log(elemObj1.fun === elemObj2.fun); // true
控制臺輸出結果如下
如下示例:
// 未用原型寫法,普通寫法求和
var arr1 = [1,2,3,4,5,6,7,8,9,10];
var arr2 = [2,4,6,8,10,12,14,16]
arr1.sum = function(){
var result = 0;
for(var i = 0;i<arr1.length;i++){ // 這里也可以換成this.length
result += this[i];
}
return result; // 返回結果
}
arr2.sum = function(){
var result = 0;
for(var i = 0;i<arr2.length;i++){
result += this[i];
}
return result; // 返回結果
}
console.log("數組arr1和為",arr1.sum()); // 55
console.log("數組arr2和為",arr2.sum()); // 72
控制臺截圖如下:
原型寫法
// 原型寫法
var arr1 = [1,2,3,4,5,6,7,8,9,10];
var arr2 = [2,4,6,8,10,12,14,16]
Array.prototype.sum = function(){
var result = 0;
for(var i = 0;i<this.length;i++){
result += this[i];
}
return result;
}
console.log("數組arr1的和為",arr1.sum()); // 數組arr1的和為55
console.log("數組arr2的和為",arr2.sum()); // 數組arr2的和為72
console.log(arr1.sum === arr1.sum); // true
//普通函數封裝寫法,也就是閉包寫法
var arr1 = [1,2,3,4,5,6,7,8,9,10];
var arr2 = [2,4,6,8,10,12,14,16]
function AddResult(arr){
arr.sum = function(){
var result = 0;
for(var i = 0;i<this.length;i++){ // 這里也可以換成this.length
result += this[i];
}
return result; // 返回結果
}
return arr.sum();
}
console.log("數組arr1和為",AddResult(arr1)); // 數組arr1和為55
console.log("數組arr2和為",AddResult(arr2)); // 數組arr2和為72
區分構造函數自定義屬性與原型屬性
如下示例代碼所示:
function Person(name,publicNum){
this.name = name;
this.publicNum = publicNum;
this.userDefined = function(){
return "我是構造函數自定義方法unerDefined"
}
}
Person.prototype.age = 25;
Person.prototype.init = {
city:"beijing",
job:"coder",
method:function(){
return "我是原型下的方法輸出"
}
}
// 定義鑒別原型屬性方法
function hasPrototypeProperty(object,variable){
return !object.hasOwnProperty(variable) && (variable in object);
}
var person = new Person("隨筆川跡","itclancoder");
console.log(person.name,person.publicNum,person.userDefined(),person.init.city,person.init.job,person.init.method());
console.log(hasPrototypeProperty(person,"name"));
console.log(person.hasOwnProperty("name"));
console.log(person.hasOwnProperty("age")); // hasOwnProperty只能檢測自定義屬性,false
console.log(hasPrototypeProperty(person,"age"));
console.log("age" in person); // true,in操作符既能檢測自定義屬性也能檢測出原型下的屬性
控制臺輸出結果如下:
使用對象字面量形式改寫原型對象會改變構造函數的屬性,指向問題
function Person(name,job){
this.name = name;
this.job = job;
}
Person.prototype.init = {
name:"小川",
job:"碼男",
outName:function(){
return this.name;
},
outJob:function(){
return this.job;
}
}
var person = new Person("隨筆川跡","coder");
console.log(person.name,person.job);
console.log(person.init.outName(),person.init.outJob());
console.log(person.constructor === Person); // true
console.log(person instanceof Person); // true
控制臺輸出結果如下:
若將上面的代碼更改如下:
function Person(name,job){
this.name = name;
this.job = job;
}
// 使用對象字面量形式改寫原型對象
Person.prototype ={
name:"小川",
job:"碼男",
outName:function(){
return this.name;
},
outJob:function(){
return this.job;
}
}
var person = new Person("隨筆川跡","coder");
console.log(person.name,person.job);
console.log(person.outName(),person.outJob());
console.log(person.constructor === Person); // false
console.log(person.constructor === Object); // true
console.log(person instanceof Person); // true
控制臺輸出結果如下
正確寫法
:當一個函數被創建時,它的prototype屬性也被創建,且該原型對象的constructor屬性指向該函數,當使用對象字面量形式改寫原型對象Person.prototype時,則該constructor指向指向的是Object,為了避免這一點,需要手動的改寫原型對象手動設置constructor屬性,更改如下:
function Person(name,job){
this.name = name;
this.job = job;
}
// 使用對象字面量形式改寫原型獨享
Person.prototype ={
constructor:Person, // 手動指定這里的指向該構造函數
outName:function(){
return this.name;
},
outJob:function(){
return this.job;
}
}
var person = new Person("隨筆川跡","coder");
console.log(person.name,person.job);
console.log(person.outName(),person.outJob());
console.log(person.constructor === Person); // true
console.log(person.constructor === Object); // false
console.log(person instanceof Person); // true
在原有的對象基礎上上,通過prototype進行額外的,封裝,拓展
實例代碼如下:
// 通過原型prototype對現有的內容進行額外的拓展,給數組Array添加方法
Array.prototype.sum = function(){
return this.reduce(function(m,n){
return m+n;
})
}
var arrNums = [1,2,3,4,5,6,7,8,9,10];
var result = arrNums.sum();
console.log("arrNums的和為",result); // arrNums的和為 55
// 給String添加額外的方法
String.prototype.capitalize = function(){
return this.charAt(0).toUpperCase()+this.substring(1);
}
var message = "suibichuanji hello";
console.log(message.capitalize()); // Suibichuanji hello
控制臺輸出如下:
以上例子中,我們是可以通過對系統提供的內置對象進行額外拓展的,也就是說系統對象(Date,String,Object,Array,RegExp等)是構造函數,當現有提供的功能沒法滿足時,就可以根據prototype進行拓展,因此都有原型對象給你去改變,在該新增的方法前面添加構造函數.prototype就可以了,上面的例子中是給Array.prototype添加了一個sum()求和的方法,該方法對數組所有元素進行求和并返回,arrNums數組通過原型對象自動就有了這個sum()方法,在sum()方法內部,this指向數組對象實例arrNums,所以該方法也可以使用數組的其他方法,什么reduce(),substring(),等都可以
原型中的屬性優先級
示例代碼如下所示
var arr = [];
arr.name = "隨筆川跡";
Array.prototype.name = "川流不息";
console.log(arr.name); // 隨筆川跡
控制臺輸出如下
從上結果中可以得出:當構造函數自定義的屬性名與該構造函數下原型屬性名相同時,
構造函數的自定義屬性優先于原型屬性
(可以把構造函數理解為內聯樣式),而原型屬性或者原型方法可以看做是class)小結:構造函數就是用new關鍵字調用的普通函數,可以隨時定義自己的構造函數來創建多個具有同樣的屬性的對象,可以用instanceof操作符(建議用這個)者直接訪問constructor屬性來鑒別對象是被哪個構造函數創建的,每一個函數都具有prototype屬性,它定義了構造函數所有對象共享屬性
- 自定義的屬性和方法放在構造函數里面
- 凡是共享的屬性和方法掛載在該構造函數原型下面
- javascript的查找變量的機制,是沿著作用域鏈逐級向上查找的,在原型里,是原型鏈,構造函數與原型之間的連接就是原型鏈,當訪問對象的某個屬性時,js首先在自定義的屬性的作用域內查找該變量是否存在,如果不存在,則會沿著原型鏈向原型下的查找該屬性,直至頂層Object的原型對象,若有則返回,若無,則返回undefined
面向對象小實例
效果圖:
css層疊樣式代碼
*{
padding:0;
margin:0;
}
#wrap{
width:300px;
height:260px;
border:1px solid #ccc;
margin:0 auto;
}
#wrap:after{
content:"";
height:0;
display:block;
clear:both;
zoom:1;
}
#wrap div{
height:100%;
display:none;
text-indent:10px;
background:#2263A3;
color:#fff;
}
#wrap div:nth-of-type(1){
display:block;
}
#wrap input.active{
background:#2263A3;
color:#fff;
}
#wrap input{
width:100px;
height:30px;
background:#abcdef;
text-align:center;
line-height:30px;
outline:none;
border:none;
float:left;
cursor:pointer;
margin-bottom:30px;
}
html結構
<div id="wrap">
<input type="button" class="active" value="公告" name="">
<input type="button" value="規則" name="">
<input type="button" value="論壇" name="">
<div>歡迎關注微信itclancoder公眾號</div>
<div>點擊右上方藍字即可關注</div>
<div>什么都沒有留下</div>
</div>
js代碼
// 普通寫法
// 獲取元素
var oWrap = document.querySelector("#wrap");
var aBtns = oWrap.getElementsByTagName("input");
var aDivs = oWrap.getElementsByTagName("div");
// 循環
for(var i = 0;i<aBtns.length;i++){
aBtns[i].index = i; //添加索引
aBtns[i].onclick = function(){ // 添加事件
for(var j = 0;j<aBtns.length;j++){
aBtns[j].className = ""; // 先去除掉所有的className
aDivs[j].style.display = "none"; // 先隱藏
}
// 添加class
this.className = "active";
aDivs[this.index].style.display = "block"; // 內容顯示
}
}
jquery寫法
$(function(){
$(" #wrap input").click(function(){
var $index = $(this).index(); // 獲取索引
$(this).addClass("active").siblings().removeClass("active");
$("#wrap div").eq($index).show().siblings("div").hide();
})
})```
`面向對象寫法`
function 構造函數(){
this.屬性 // 對象.屬性
}
構造函數.原型.方法 = function(){}
var 對象1 = new 構造函數();
對象1.方法();
`面向對象選項卡代碼示例如下所示`
window.onload = function(){
var t = new TabSelect(); // 實例化對象
t.init(); // 實例化對象調用方法
}
// 聲明構造函數
function TabSelect(){
// this == TabSelect,添加自定義屬性,獲取對象
this.oWrap = document.querySelector("#wrap");
this.aBtns = this.oWrap.getElementsByTagName("input");
this.aDivs = this.oWrap.getElementsByTagName("div");
}
// 構造函數的原型下添加方法(初始化)
TabSelect.prototype.init = function(){
var that = this; // 注意這里的this指向,是TabSelect,用一個局部變量將this給存儲起來,其實這種方式是根據詞法作用域,閉包的方式來解決的
for(var i = 0;i<this.aBtns.length;i++){
this.aBtns[i].index = i; //添加索引
this.aBtns[i].onclick = function(){
that.change(this);
//console.log(this);匿名函數里面的this指向的是input按鈕元素
};
}
}
// 構造器函數原型對象下添加方法
TabSelect.prototype.change = function(obj){
//console.log(obj); // input點擊按鈕元素
for(var j = 0;j<this.aBtns.length;j++){
this.aBtns[j].className = ""; // 先去除掉所有的className
this.aDivs[j].style.display = "none"; // 先隱藏
}
// 添加class
obj.className = "active";
this.aDivs[obj.index].style.display = "block"; // 內容顯示
}
`小結`:
本例從普通寫法,jquery寫法,在到最后面向對象選項卡寫法,完成一簡易的選項卡,其中jquery寫法最為簡單,容易懂,但是這里我只是為了嘗試用面向對象的思想去寫應用,實際開發中,無論哪種方式,只要能實現出來就行,從普通的寫法,也就是原生js寫法,到面向對象寫法,可以看出首先通過變形,把局部的功能給拎出來,封裝成一個函數,這個過程中盡量不要出現函數嵌套函數,因為this是指向是個令人頭疼的問題,可以有全局變量,window.onload里面盡量是實例化對象,與對象的調用的方式,把不是賦值的語句單獨放到一個函數當中(比如上文中的獲取元素,給TabSelect添加自定義屬性),最后就是改變this指向問題,事件或者定時器,讓面向對象中的this指向該對象
`總結`:
本篇主要是本人對構造器函數與原型對象的一點點理解,new操作符調用的函數為構造函,功能上與內置的函數并沒有多大的區別,構造函數首字母大寫用來區分普通函數還是構造函數,構造函數中的this指向該實例化的構造函數,主要是創建多個共享特定屬性和行為的對象,用于創建模板,作為餅干工具,而原型對象主要是改寫構造函數(對象)下面的方法和屬性,讓公用方法或者屬性在內存中存在一份,解決了當創建多個實例化對象時,重復的創建構造函數的過程,目的是減少內存開銷,提高性能,還有就是原型在原有的對象基礎上,通過prototype進行額外的,封裝,拓展,原型是掛載在構造函數下面的,以及最后用面向對象寫法實現了一個小實例,其實設計模式中的原型模式就是面向對象的寫法,殺雞焉用牛刀,適合自己的才是好方法,面向對象的寫法,對于簡單的實例,面向過程就可以了,對于復雜的實例,什么組件,插件,我覺得都是面向對象的應用,關于面向對象,我也只是略知皮毛,在不斷的學習當中...
`以下是本篇提點概要`
* 什么是函數:function關鍵字聲明,一獨立封閉功能的代碼塊,也是對象
* 什么是構造函數:new關鍵字創建對象時調用的函數,用于創建模板,生成餅干工具
* 普通函數與構造函數的區別,有new無new區別,this的指向,普通函數,this指向全局window,而構造器函數this,指向該new 構造器函數調用
* 為何內置構造函數無new也能工作,因為那些內置系統構造函數,都被設計為作用域安全的構造函數,一個作用域安全的構造函數無new也可以工作,并返回同樣類型的對象
* 原型對象,prototype,函數一旦聲明,就有該屬性,作用1:去改寫對象下面公用的方法或者屬性,讓公用方法或者屬性在內存中存在一份,可以看作是對象的基類
作用2:在原有的對象基礎上上,通過prototype進行額外的,封裝,拓展
* 區分構造函數自定義屬性與原型屬性,用in操作符,hasOwnProperty組合使用進行判斷(見上示例代碼)
* 使用對象字面量形式改寫原型對象會改變構造函數的屬性,指向問題,需手動的改寫原型對象手動設置constructor屬性
* 在原有的對象基礎上,通過prototype進行額外的,封裝,拓展
* 原型中的屬性優先級,構造函數自定義的屬性優先級優先于原型屬性,查找變量的方式是沿著原型鏈逐級查找的,直至頂層Object,有則返回,無則返回undeinfed
* 面向對象小實例,普通寫法,JQuery寫法與面向對象選項卡寫法