前言:雖然是後端開發,但是總是會有前端的工作,在這裏學習了一下JS的函數與對象做了一些瞭解,以便更好地學習Vue,因爲對這塊還不是很熟,所以基本從別的博客拼湊過來的加上自己的推斷,寫的有不準確的地方請多多包含。
參考:
https://www.cnblogs.com/honkerzh/p/10270624.html
https://www.cnblogs.com/juggdxy/p/8245491.html
https://blog.csdn.net/qq_42497250/article/details/92845285
https://blog.csdn.net/cc18868876837/article/details/81211729
https://blog.csdn.net/q610376681/article/details/102648808
https://blog.csdn.net/qtfying/article/details/105147755
目錄
第一章 JS函數
1.1 函數
首先弄明白何爲函數呢,函數簡單的說就是重複執行的代碼塊。函數是這樣的一段JavaScript 代碼,它只定義一次,但可能被執行或調用任意次。
函數的定義方式:
1.聲明式函數定義: function 函數名 (){};這種定義方式,會將函數聲明提升到該函數所在作用域的最開頭,也是就無論你在這個函數的最小作用域的那兒使用這種方式聲明的函數,在這個作用域內,你都可以調用這個函數爲你所用。
2.函數表達式:let fun = function(){}; 此方式定義的函數,只能在該作用域中,這段賦值代碼執行之後才能通過fun()調用函數,否則,由於變量聲明提升,fun === undefined。
3.new Function 形式: var fun1 = new Function (arg1 , arg2 ,arg3 ,…, argN , body );Function構造函數所有的參數都是字符串類型。除了最後一個參數, 其餘的參數都作爲生成函數的參數即形參。這裏可以沒有參數。最後一個參數, 表示的是要創建函數的函數體。
總結:1 、第一種和第二種函數的定義的方式其實是第三種new Function 的語法糖,當我們定義函數時候都會通過 new Function 來創建一個函數,只是前兩種爲我們進行了封裝,我們看不見了而已,js 中任意函數都是Function 的實例。
2、ECMAScript 定義的 函數實際上是功能完整的對象
1.2 私有變量與函數
在函數內定義的變量和函數如果不對外提供接口,那麼外部將無法訪問到,也就是變爲私有變量和私有函數。
function Obj(){
var a=0; //私有變量
var fn=function(){ //私有函數
}
}
這樣在函數對象Obj外部無法訪問變量a和函數fn,它們就變成私有的,只能在Obj內部使用,即使是函數Obj的實例仍然無法訪問這些變量和函數。
var o=new Obj();
console.log(o.a); //undefined
console.log(o.fn); //undefined
當定義一個函數後通過 “.”爲其添加的屬性和函數,通過對象本身仍然可以訪問得到,但是其實例卻訪問不到,這樣的變量和函數分別被稱爲靜態變量和靜態函數,用過Java、C#的同學很好理解靜態的含義。
function Obj(){
}
Obj.a=0; //靜態變量
Obj.fn=function(){ //靜態函數 }
console.log(Obj.a); //0
console.log(typeof Obj.fn); //function
var o=new Obj();
console.log(o.a); //undefined
console.log(typeof o.fn); //undefined
1.3 prototype
1.3.1 來由
在面向對象編程中除了一些庫函數我們還是希望在對象定義的時候同時定義一些屬性和方法,實例化後可以訪問,JavaScript也能做到這樣
function Obj(){
this.a=[]; //實例變量
this.fn=function(){ //實例方法
}
}
console.log(typeof Obj.a); //undefined
console.log(typeof Obj.fn); //undefined
var o=new Obj();
console.log(typeof o.a); //object
console.log(typeof o.fn); //function
這樣可以達到上述目的,然而
function Obj(){
this.a=[]; //實例變量
this.fn=function(){ //實例方法
}
}
var o1=new Obj();
o1.a.push(1);
o1.fn={};
console.log(o1.a); //[1]
console.log(typeof o1.fn); //object
var o2=new Obj();
console.log(o2.a); //[]
console.log(typeof o2.fn); //function
上面的代碼運行結果完全符合預期,但同時也說明一個問題,在o1中修改了a和fn,而在o2中沒有改變,由於數組和函數都是對象,是引用類型,
這就說明o1中的屬性和方法與o2中的屬性與方法雖然同名但卻不是一個引用,而是對Obj對象定義的屬性和方法的一個複製。
這個對屬性來說沒有什麼問題,但是對於方法來說問題就很大了,因爲方法都是在做完全一樣的功能,但是卻又兩份複製,如果一個函數對象有上千和實例方法,
那麼它的每個實例都要保持一份上千個方法的複製,這顯然是不科學的,這可腫麼辦呢,prototype應運而生。
1.3.2 應用
無論什麼時候,只要創建了一個新函數,就會根據一組特定的規則爲該函數創建一個prototype屬性,默認情況下prototype屬性會默認獲得一個constructor(構造函數)屬性,這個屬性是一個指向prototype屬性所在函數的指針。
function Person(){
}
根據上圖可以看出Person對象會自動獲得prototyp屬性,而prototype也是一個對象,會自動獲得一個constructor屬性,該屬性正是指向Person對象。
當調用構造函數創建一個實例的時候,實例內部將包含一個內部指針(很多瀏覽器這個指針名字爲__proto__)指向構造函數的prototype,這個連接存在於實例和構造函數的prototype之間,而不是實例與構造函數之間 。
function Person(name){
this.name=name;
}
Person.prototype.printName=function(){
alert(this.name);
}
var person1=new Person('Byron');
var person2=new Person('Frank');
Person的實例person1中包含了name屬性,同時自動生成一個__proto__屬性,該屬性指向Person的prototype,可以訪問到prototype內定義的printName方法,大概就是這個樣子的
1.3.3 測試
寫段程序測試一下看看prototype內屬性、方法是能夠共享
function Person(name){
this.name=name;
}
Person.prototype.share=[];
Person.prototype.printName=function(){
alert(this.name);
}
var person1=new Person('Byron');
var person2=new Person('Frank');
person1.share.push(1);
person2.share.push(2);
console.log(person2.share); //[1,2]
果不其然!實際上當代碼讀取某個對象的某個屬性的時候,都會執行一遍搜索,目標是具有給定名字的屬性,搜索首先從對象實例開始,如果在實例中找到該屬性則返回,
如果沒有則查找prototype,如果還是沒有找到則繼續遞歸prototype的prototype對象,直到找到爲止,如果遞歸到object仍然沒有則返回錯誤。
同樣道理如果在實例中定義如prototype同名的屬性或函數,則會覆蓋prototype的屬性或函數。
function Person(name){
this.name=name;
}
Person.prototype.share=[];
var person=new Person('Byron');
person.share=0;
console.log(person.share); //0而不是prototype中的[]
1.3.4 總結
當然prototype不是專門爲解決上面問題而定義的,但是卻解決了上面問題。瞭解了這些知識就可以構建一個科學些的、複用率高的對象,如果希望實例對象的屬性或函數則定義到prototype中,
如果希望每個實例單獨擁有的屬性或方法則定義到this中,可以通過構造函數傳遞實例化參數。
function Person(name){
this.name=name;
}
Person.prototype.share=[];
Person.prototype.printName=function(){
alert(this.name);
}
1.4 構造函數
定義:通過 new 函數名 來實例化對象的函數叫構造函數。任何的函數都可以作爲構造函數存在。之所以有構造函數與普通函數之分,主要從功能上進行區別的,構造函數的主要 功能爲 初始化對象,特點是和new 一起使用。new就是在創建對象,從無到有,構造函數就是在爲初始化的對象添加屬性和方法。構造函數定義時首字母大寫(規範)。
對new理解:new 申請內存, 創建對象,當調用new時,後臺會隱式執行new Object()創建對象。所以,通過new創建的字符串、數字是引用類型,而是非值類型。
var arr = []; 爲 var arr = new Array(); 的語法糖。
var obj = {} 爲 var obj = new Object(); 的語法糖
var date = new Date();
1.4 函數作用域
1.4.1 不同代碼塊之間沒有變量提升
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
</body>
<script>
var c ="www.baidu.com"
out(c)
</script>
<script>
function out(url){
console.log(url)
}
</script>
</html>
結果:out未定義
1.4.2 同一代碼塊種有變量提升
console.log(foo); // foo() {console.log(123);}
console.log(foo());//undefined 爲啥是undefined下面我會說
var foo = 456;
function foo() {
console.log(123);//123
}
console.log(foo); //undefined
foo = 789;
console.log(foo);//456
console.log(foo());// 789
以上代碼等同於:
var foo; // (0) 聲明foo,並賦值爲undefined
foo = function() { // (1-1) 將一個函數賦值給foo
console.log(123); //(2-1) 執行函數,打印123
}
console.log(foo); //(1-2) foo此時爲函數體的引用
console.log(foo()); //(2-2)首先執行foo()函數,進入(2-1),執行完畢後,函數未有返回體即return,故打印undefined
foo = 456; //(3)再次給foo重新複製一個基本數據類型的456
console.log(foo); // (4)打印出456
// 再次賦值
foo = 789 // (5)又再一次給foo重新複製一個基本數據類型的789
console.log(foo); // (6)打印出789
console.log(foo()); // (7)此時foo非函數體引用,故不能使用函數調用方法,故報錯 foo is not a function
函數定義會跑到script塊的最前面,所以最開始能打印出foo函數,但在不同scritp塊中就不行了,個人有個很有意思的疑問,如果有兩個函數,a函數用b函數的變量,b函數用a函數的變量,那麼是不是變量提升也沒用了,肯定會有一個是undefined,還是說JS引擎會先掃描整個JS代碼,把所有全局變量都綁定到window對象上,包括函數和變量的定義。此時,window上就有了函數和值爲undefined,這個後面嘗試一下。
來個更高難度的:
console.log(person)
console.log(fun)
var person = '樂鳴'
console.log(person)
function fun () {
console.log(person)
var person = '秦時明月'
console.log(person)
}
fun()
console.log(person)
以上代碼塊等同於:
var fun;
fun = function() {
console.log(person)
var person = '秦時明月'
console.log(person)
};
console.log(person); // undefined
console.log(fun); // 函數體function(){...}
var person = '清風';
console.log(person); // 清風
fun(); // 執行fun函數 // undefined // 秦時明月
console.log(person); // 清風
爲啥類似於如下這樣一個函數,打印出來會是undefined呢?
var person = '清風明月';
function fun(){
console.log(person);
var person = '秦時明月';
console.log(person);
}
因爲:
代碼在執行時,首先會檢查person是否爲函數私有變量,如果是,則停止查找全局變量中的person,並且該函數中的person僅僅在函數體中生效,所以當檢測到person爲fun的私有變量是,如果前置,則直接返回undefined也就理所當然了
第二章 創建對象的方法
2.1 new 操作符 + Object 創建對象
var person = new Object();
person.name = "lisi";
person.age = 21;
person.family = ["lida","lier","wangwu"];
person.say = function(){
alert(this.name);
}
2.2 字面式創建對象
var person ={
name: "lisi",
age: 21,
family: ["lida","lier","wangwu"],
say: function(){
alert(this.name);
}
};
以上兩種方法在使用同一接口創建多個對象時,會產生大量重複代碼,爲了解決此問題,工廠模式被開發。
2.3 工廠模式
function createPerson(name,age,family) {
var o = new Object();
o.name = name;
o.age = age;
o.family = family;
o.say = function(){
alert(this.name);
}
return o;
}
var person1 = createPerson("lisi",21,["lida","lier","wangwu"]); //instanceof無法判斷它是誰的實例,只能判斷他是對象,構造函數都可以判斷出
var person2 = createPerson("wangwu",18,["lida","lier","lisi"]);
console.log(person1 instanceof Object); //true
工廠模式解決了重複實例化多個對象的問題,但沒有解決對象識別的問題(但是工廠模式卻無從識別對象的類型,因爲全部都是Object,不像Date、Array等,本例中,得到的都是o對象,對象的類型都是Object,因此出現了構造函數模式)
2.4 構造函數模式
function Person(name,age,family) {
this.name = name;
this.age = age;
this.family = family;
this.say = function(){
alert(this.name);
}
}
var person1 = new Person("lisi",21,["lida","lier","wangwu"]);
var person2 = new Person("lisi",21,["lida","lier","lisi"]);
console.log(person1 instanceof Object); //true
console.log(person1 instanceof Person); //true
console.log(person2 instanceof Object); //true
console.log(person2 instanceof Person); //true
console.log(person1.constructor); //constructor 屬性返回對創建此對象的數組、函數的引用
對比工廠模式有以下不同之處:
1、沒有顯式地創建對象
2、直接將屬性和方法賦給了 this 對象
3、沒有 return 語句
以此方法調用構造函數步驟 {
1、創建一個新對象
2、將構造函數的作用域賦給新對象(將this指向這個新對象)
3、執行構造函數代碼(爲這個新對象添加屬性)
4、返回新對象 ( 指針賦給變量person ??? )
}
可以看出,構造函數知道自己從哪裏來(通過 instanceof 可以看出其既是Object的實例,又是Person的實例)
構造函數也有其缺陷,每個實例都包含不同的Function實例( 構造函數內的方法在做同一件事,但是實例化後卻產生了不同的對象,方法是函數 ,函數也是對象)詳情見構造函數詳解
因此產生了原型模式
2.5 原型模式
function Person() {
}
Person.prototype.name = "lisi";
Person.prototype.age = 21;
Person.prototype.family = ["lida","lier","wangwu"];
Person.prototype.say = function(){
alert(this.name);
};
console.log(Person.prototype); //Object{name: 'lisi', age: 21, family: Array[3]}
var person1 = new Person(); //創建一個實例person1
console.log(person1.name); //lisi
var person2 = new Person(); //創建實例person2
person2.name = "wangwu";
person2.family = ["lida","lier","lisi"];
console.log(person2); //Person {name: "wangwu", family: Array[3]}
// console.log(person2.prototype.name); //報錯
console.log(person2.age); //21
原型模式的好處是所有對象實例共享它的屬性和方法(即所謂的共有屬性),此外還可以如代碼第16,17行那樣設置實例自己的屬性(方法)(即所謂的私有屬性),可以覆蓋原型對象上的同名屬性(方法),但我想缺點也是顯而易見的(個人推測),prototype的使用使所有對象的屬性值是一樣的,一個對象的屬性修改,其它同類對象的屬性都要修改。具體參見原型模式詳解
2.6 混合模式
function Person(name,age,family){
this.name = name;
this.age = age;
this.family = family;
}
Person.prototype = {
constructor: Person, //每個函數都有prototype屬性,指向該函數原型對象,原型對象都有constructor屬性,這是一個指向prototype屬性所在函數的指針
say: function(){
alert(this.name);
}
}
var person1 = new Person("lisi",21,["lida","lier","wangwu"]);
console.log(person1);
var person2 = new Person("wangwu",21,["lida","lier","lisi"]);
console.log(person2);
可以看出,混合模式共享着對相同方法的引用,又保證了每個實例有自己的私有屬性,最大限度的節省了內存。