摘要:汪汪汪哈士奇大黃狗輸出結(jié)果為這樣寫依然存在問題全局變量增多,會(huì)增加引入框架命名沖突的風(fēng)險(xiǎn)代碼結(jié)構(gòu)混亂,會(huì)變得難以維護(hù)想要解決上面的問題就需要用到構(gòu)造函數(shù)的原型概念
JS高級(jí) 前言
經(jīng)過前面幾篇文章的學(xué)習(xí),相信大家已經(jīng)對(duì)js有了大部分的理解了,但是要想真正的掌握好js,本篇才是關(guān)鍵。由于js高級(jí)階段的知識(shí)點(diǎn)比較難理解,所以本篇文章花了大量的時(shí)間去理思路,有可能有一些知識(shí)點(diǎn)遺漏了,也有可能有部分知識(shí)點(diǎn)寫的不對(duì),歡迎大家留言糾正。1.異常處理
常見的異常分類
運(yùn)行環(huán)境的多樣性導(dǎo)致的異常(瀏覽器)
語法錯(cuò)誤,代碼錯(cuò)誤
異常最大的特征,就是一旦代碼出現(xiàn)異常,后面的代碼就不會(huì)執(zhí)行。
1.1異常捕獲捕獲異常,使用try-catch語句:
try{ // 這里寫可能出現(xiàn)異常的代碼 }catch(e){ // e-捕獲的異常對(duì)象 // 可以在此處書寫出現(xiàn)異常后的處理代碼 }
異常捕獲語句執(zhí)行的過程為:
代碼正常運(yùn)行, 如果在try中出現(xiàn)了錯(cuò)誤,try里面出現(xiàn)錯(cuò)誤的語句后面的代碼都不再執(zhí)行, 直接跳轉(zhuǎn)到catch中
catch中處理錯(cuò)誤信息
然后繼續(xù)執(zhí)行后面的代碼
如果try中沒有出現(xiàn)錯(cuò)誤, 那么不走catch直接執(zhí)行后面的代碼
通過try-catch語句進(jìn)行異常捕獲之后,代碼將會(huì)繼續(xù)執(zhí)行,而不會(huì)中斷。
示例代碼:
console.log("代碼開始執(zhí)行"); try{ console.log(num); // num 在外部是沒有定義的 }catch(e){ console.log(e); console.log("我已經(jīng)把錯(cuò)誤處理了"); } console.log("代碼結(jié)束執(zhí)行");
效果圖:
從效果圖中我們可以看到,num是一個(gè)沒有定義的變量,如果沒有放在try-catch代碼塊中,后面的‘代碼結(jié)束執(zhí)行’就不會(huì)被打印。通過把try-catch放在代碼塊中,出現(xiàn)錯(cuò)誤后,就不會(huì)影響后面代碼的運(yùn)行了,他會(huì)把錯(cuò)誤信息打印出來。
注意:
語法錯(cuò)誤異常用try-catch語句無法捕獲,因?yàn)樵陬A(yù)解析階段,語法錯(cuò)誤會(huì)直接檢測出來,而不會(huì)等到運(yùn)行的時(shí)候才報(bào)錯(cuò)。
try-catch在一般日常開發(fā)中基本用不到,但是如果要寫框架什么的,用的會(huì)非常多。因?yàn)檫@個(gè)會(huì)讓框架變得健壯
異常捕獲語句的完整模式
異常捕獲語句的完整模式為try-catch-finally
try { //可能出現(xiàn)錯(cuò)誤的代碼 } catch ( e ) { //如果出現(xiàn)錯(cuò)誤就執(zhí)行 } finally { //結(jié)束 try 這個(gè)代碼塊之前執(zhí)行, 即最后執(zhí)行 }
finally中的代碼,不管有沒有發(fā)生異常,都會(huì)執(zhí)行。一般用在后端語言中,用來釋放資源,JavaScript中很少會(huì)用到
1.2拋出異常如何手動(dòng)的拋出異常呢?
案例:自己寫的一個(gè)函數(shù),需要一個(gè)參數(shù),如果用戶不傳參數(shù),此時(shí)想直接給用戶拋出異常,就需要了解如何拋出異常。
拋出異常使用throw關(guān)鍵字,語法如下:
throw 異常對(duì)象;
異常對(duì)象一般是用new Error("異常消息"), 也可以使用任意對(duì)象
示例代碼:
function test(para){ if(para == undefined){ throw new Error("請(qǐng)傳遞參數(shù)"); //這里也可以使用自定義的對(duì)象 throw {"id":1, msg:"參數(shù)未傳遞"}; } } try{ test(); }catch(e){ console.log(e); }
效果圖:
1.3異常的傳遞機(jī)制function f1 () { f2(); } function f2 () { f3(); } function f3() { throw new Error( "error" ); } f1(); // f1 稱為調(diào)用者, 或主調(diào)函數(shù), f2 稱為被調(diào)用者, 或被調(diào)函數(shù)
當(dāng)在被調(diào)函數(shù)內(nèi)發(fā)生異常的時(shí)候,異常會(huì)一級(jí)一級(jí)往上拋出。
2.面向?qū)ο缶幊?/b>在了解面向?qū)ο缶幊讨?,我們先來了解下什么是面向過程,什么是面向?qū)ο?,他們之間的區(qū)別是什么。2.1 面向過程和面向?qū)ο蟮牡膶?duì)比
舉個(gè)例子:
日常洗衣服
1.面向過程的思維方式:
面向過程編程:將解決問題的關(guān)注點(diǎn)放在解決問題的具體細(xì)節(jié)上,關(guān)注如何一步一步實(shí)現(xiàn)代碼細(xì)節(jié);
step 1:收拾臟衣服 step 2:打開洗衣機(jī)蓋 step 3:將臟衣服放進(jìn)去 step 4:設(shè)定洗衣程序 step 5:開始洗衣服 step 6:打開洗衣機(jī)蓋子 step 7:曬衣服
2.面向?qū)ο蟮乃季S方式:
面向?qū)ο缶幊蹋簩⒔鉀Q問題的關(guān)注點(diǎn)放在解決問題所需的對(duì)象上,我們重點(diǎn)找對(duì)象;
人(對(duì)象) 洗衣機(jī)(對(duì)象)
在面向?qū)ο蟮乃季S方式中:我們只關(guān)心要完成事情需要的對(duì)象,面向?qū)ο笃鋵?shí)就是對(duì)面向過程的封裝;
示例代碼:
在頁面上動(dòng)態(tài)創(chuàng)建一個(gè)元素
//面向過程 //1-創(chuàng)建一個(gè)div var div=document.createElement("div"); //2-div設(shè)置內(nèi)容 div.innerHTML="我是div"; //3-添加到頁面中 document.body.appendChild(div); //面向?qū)ο?$("body").append("我也是div");
我們可以看出,jQ封裝的其實(shí)就是對(duì)面向過程的封裝。
總結(jié): 面向?qū)ο笫且环N解決問題的思路,一種編程思想。
2.2 面向?qū)ο缶幊膛e例設(shè)置頁面中的div和p的邊框?yàn)?b>"1px solid red"
1、傳統(tǒng)的處理辦法
// 1> 獲取div標(biāo)簽 var divs = document.getElementsByTagName( "div" ); // 2> 遍歷獲取到的div標(biāo)簽 for(var i = 0; i < divs.length; i++) { //3> 獲取到每一個(gè)div元素,設(shè)置div的樣式 divs[i].style.border = "1px dotted black"; } // 4> 獲取p標(biāo)簽 var ps = document.getElementsByTagName("p"); // 5> 遍歷獲取到的p標(biāo)簽 for(var j = 0; j < ps.length; j++) { // 獲取到每一個(gè)p元素 設(shè)置p標(biāo)簽的樣式 ps[j].style.border = "1px dotted black"; }
2、使用函數(shù)進(jìn)行封裝優(yōu)化
// 通過標(biāo)簽名字來獲取頁面中的元素 function tag(tagName) { return document.getElementsByTagName(tagName); } // 封裝一個(gè)設(shè)置樣式的函數(shù) function setStyle(arr) { for(var i = 0; i < arr.length; i++) { // 獲取到每一個(gè)div或者p元素 arr[i].style.border = "1px solid #abc"; } } var dvs = tag("div"); var ps = tag("p"); setStyle(dvs); setStyle(ps);
3、使用面向?qū)ο蟮姆绞?/strong>
// 更好的做法:是將功能相近的代碼放到一起 var obj = { // 命名空間 getEle: { tag: function (tagName) { return document.getElementsByTagName(tagName); }, id: function (idName) { return document.getElementById(idName); } // ... }, setCss: { setStyle: function (arr) { for(var i = 0; i < arr.length; i++) { arr[i].style.border = "1px solid #abc"; } }, css: function() {}, addClass: function() {}, removeClass: function() {} // ... } // 屬性操作模塊 // 動(dòng)畫模塊 // 事件模塊 // ... }; var divs = obj.getEle.tag("div"); obj.setCss.setStyle(divs);2.3 面向?qū)ο蟮娜筇匦?/b>
面向?qū)ο蟮娜筇匦苑謩e是:"封裝","繼承","多態(tài)"。
1、封裝性
對(duì)象就是對(duì)屬性和方法的封裝,要實(shí)現(xiàn)一個(gè)功能,對(duì)外暴露一些接口,調(diào)用者只需通過接口調(diào)用即可,不需要關(guān)注接口內(nèi)部實(shí)現(xiàn)原理。
js對(duì)象就是“鍵值對(duì)”的集合
鍵值如果是數(shù)據(jù)( 基本數(shù)據(jù), 復(fù)合數(shù)據(jù), 空數(shù)據(jù) ), 就稱為屬性
如果鍵值是函數(shù), 那么就稱為方法
對(duì)象就是將屬性與方法封裝起來
方法是將過程封裝起來
2、繼承性
所謂繼承就是自己沒有, 別人有,拿過來為自己所用, 并成為自己的東西
2.1、傳統(tǒng)繼承基于模板
子類可以使用從父類繼承的屬性和方法。
class Person { string name; int age; } class Student : Person { } var stu = new Student(); stu.name
即:讓某個(gè)類型的對(duì)象獲得另一個(gè)類型的對(duì)象的屬性的方法
2.2、js 繼承基于對(duì)象
在JavaScript中,繼承就是當(dāng)前對(duì)象可以使用其他對(duì)象的方法和屬性。
js繼承實(shí)現(xiàn)舉例:混入(mix)
// 參數(shù)o1和o2是兩個(gè)對(duì)象,其中o1對(duì)象繼承了所有o2對(duì)象的“k”屬性或者方法 var o1 = {}; var o2 = { name: "Levi", age: 18, gender: "male" }; function mix ( o1, o2 ) { for ( var k in o2 ) { o1[ k ] = o2[ k ]; } } mix(o1, o2); console.log(o1.name); // "Levi"
3、多態(tài)性(基于強(qiáng)類型,js中沒有多態(tài))只做了解
同一個(gè)類型的變量可以表現(xiàn)出不同形態(tài),用父類的變量指向子類的對(duì)象。
動(dòng)物 animal = new 子類(); // 子類:麻雀、狗、貓、豬、狐貍... 動(dòng)物 animal = new 狗(); animal.叫();2.4 創(chuàng)建對(duì)象的方式
1、字面量 {}
var student1 = { name:"諸葛亮", score:100, code:1, } var student2 = { name:"蔡文姬", score:98, code:2, } var student3 = { name:"張飛", score:68, code:3, }
字面量創(chuàng)建方式,代碼復(fù)用性太低,每一次都需要重新創(chuàng)建一個(gè)對(duì)象。
2、Object()構(gòu)造函數(shù)
var student1 = new Object(); student1.name = "諸葛亮"; student1.score = 100; student1.code = 1; var student2 = new Object(); student2.name = "蔡文姬"; student2.score = 98; student2.code = 2; var student3 = new Object(); student3.name = "張飛"; student3.score = 68; student3.code = 3;
代碼復(fù)用性太低,字面量創(chuàng)建的方式其實(shí)就是代替Object()構(gòu)造函數(shù)創(chuàng)建方式的。
3、自定義構(gòu)造函數(shù)
自定義構(gòu)造函數(shù),可以快速創(chuàng)建多個(gè)對(duì)象,并且代碼復(fù)用性高。
// 一般為了區(qū)分構(gòu)造函數(shù)與普通函數(shù),構(gòu)造函數(shù)名首字母大寫 function Student(name,score,code){ this.name = name; this.score = score; this.code = code; } var stu1 = new Student("諸葛亮",100,1); var stu2 = new Student("蔡文姬",98,2); var stu3 = new Student("張飛",68,3);
構(gòu)造函數(shù)語法:
構(gòu)造函數(shù)名首字母大寫;
構(gòu)造函數(shù)一般與關(guān)鍵字:new一起使用;
構(gòu)造函數(shù)一般不需要設(shè)置return語句,默認(rèn)返回的是新創(chuàng)建的對(duì)象;
this指向的是新創(chuàng)建的對(duì)象。
構(gòu)造函數(shù)的執(zhí)行過程:
new關(guān)鍵字,創(chuàng)建一個(gè)新的對(duì)象,會(huì)在內(nèi)存中開辟一個(gè)新的儲(chǔ)存空間;
讓構(gòu)造函數(shù)中的this指向新創(chuàng)建的對(duì)象;
執(zhí)行構(gòu)造函數(shù),給新創(chuàng)建的對(duì)象進(jìn)行初始化(賦值);
構(gòu)造函數(shù)執(zhí)行(初始化)完成,會(huì)將新創(chuàng)建的對(duì)象返回。
構(gòu)造函數(shù)的注意點(diǎn):
構(gòu)造函數(shù)本身也是函數(shù);
構(gòu)造函數(shù)有返回值,默認(rèn)返回的是新創(chuàng)建的對(duì)象;
但是如果手動(dòng)添加返回值,添加的是值類型數(shù)據(jù)的時(shí)候,構(gòu)造函數(shù)沒有影響。如果添加的是引用類型(數(shù)組、對(duì)象等)值的時(shí)候,會(huì)替換掉新創(chuàng)建的對(duì)象。
function Dog(){ this.name="哈士奇"; this.age=0.5; this.watch=function(){ console.log("汪汪汪,禁止入內(nèi)"); } // return false; 返回值不會(huì)改變,還是新創(chuàng)建的對(duì)象 // return 123; 返回值不會(huì)改變,還是新創(chuàng)建的對(duì)象 // return [1,2,3,4,5]; 返回值發(fā)生改變,返回的是這個(gè)數(shù)組 return {aaa:"bbbb"}; // 返回值發(fā)生改變,返回的是這個(gè)對(duì)象 } var d1=new Dog(); // 新創(chuàng)建一個(gè)對(duì)象 console.log(d1);
構(gòu)造函數(shù)可以當(dāng)做普通函數(shù)執(zhí)行,里面的this指向的是全局對(duì)象window。
function Dog(){ this.name="husky"; this.age=0.5; this.watch=function(){ console.log("汪汪汪,禁止入內(nèi)"); } console.log(this); // window對(duì)象 return 1; } console.log(Dog()); // 打印 12.5 面向?qū)ο蟀咐?/b>
通過一個(gè)案例,我們來了解下面向?qū)ο缶幊蹋ò咐杏幸粋€(gè)prototype概念,可以學(xué)完原型那一章后再來看這個(gè)案例)。
需求:
實(shí)現(xiàn)一個(gè)MP3音樂管理案例;
同種類型的MP3,廠家會(huì)生產(chǎn)出成百上千個(gè),但是每個(gè)MP3都有各自的樣式、使用者、歌曲;
每個(gè)MP3都有一樣的播放、暫停、增刪歌曲的功能(方法);
圖解:
示例代碼:
// 每個(gè)MP3都有自己的 主人:owner 樣式:color 歌曲:list function MP3(name,color,list){ this.owner = name || "Levi"; // 不傳值時(shí)默認(rèn)使用者是‘Levi’ this.color = color || "pink"; this.musicList = list || [ {songName:"男人哭吧不是罪",singer:"劉德華"}, {songName:"吻別",singer:"張學(xué)友"}, {songName:"對(duì)你愛不完",singer:"郭富城"}, {songName:"今夜你會(huì)不會(huì)來",singer:"黎明"} ]; } // 所有的MP3都有 播放 暫停 音樂 增刪改查的功能 MP3.prototype = { // 新增 add:function(songName,singer){ this.musicList.push({songName:songName,singer:singer}); }, // 查找 select:function(songName){ for(var i=0;i打印結(jié)果:
3.原型 3.1 傳統(tǒng)構(gòu)造函數(shù)存在問題通過自定義構(gòu)造函數(shù)的方式,創(chuàng)建小狗對(duì)象:
兩個(gè)實(shí)例化出來的“小狗”,它們都用的同一個(gè)say方法,為什么最后是false呢?function Dog(name, age) { this.name = name; this.age = age; this.say = function() { console.log("汪汪汪"); } } var dog1 = new Dog("哈士奇", 1.5); var dog2 = new Dog("大黃狗", 0.5); console.log(dog1); console.log(dog2); console.log(dog1.say == dog2.say); //輸出結(jié)果為false畫個(gè)圖理解下:
每次創(chuàng)建一個(gè)對(duì)象的時(shí)候,都會(huì)開辟一個(gè)新的空間,我們從上圖可以看出,每只創(chuàng)建的小狗有一個(gè)say方法,這個(gè)方法都是獨(dú)立的,但是功能完全相同。隨著創(chuàng)建小狗的數(shù)量增多,造成內(nèi)存的浪費(fèi)就更多,這就是我們需要解決的問題。
為了避免內(nèi)存的浪費(fèi),我們想要的其實(shí)是下圖的效果:
解決方法:
這里最好的辦法就是將函數(shù)體放在構(gòu)造函數(shù)之外,在構(gòu)造函數(shù)中只需要引用該函數(shù)即可。function sayFn() { console.log("汪汪汪"); } function Dog(name, age) { this.name = name; this.age = age; this.say = sayFn(); } var dog1 = new Dog("哈士奇", 1.5); var dog2 = new Dog("大黃狗", 0.5); console.log(dog1); console.log(dog2); console.log(dog1.say == dog2.say); //輸出結(jié)果為 true這樣寫依然存在問題:
全局變量增多,會(huì)增加引入框架命名沖突的風(fēng)險(xiǎn)
代碼結(jié)構(gòu)混亂,會(huì)變得難以維護(hù)
想要解決上面的問題就需要用到構(gòu)造函數(shù)的原型概念。
3.2 原型的概念prototype:原型。每個(gè)構(gòu)造函數(shù)在創(chuàng)建出來的時(shí)候系統(tǒng)會(huì)自動(dòng)給這個(gè)構(gòu)造函數(shù)創(chuàng)建并且關(guān)聯(lián)一個(gè)空的對(duì)象。這個(gè)空的對(duì)象,就叫做原型。關(guān)鍵點(diǎn):
每一個(gè)由構(gòu)造函數(shù)創(chuàng)建出來的對(duì)象,都會(huì)默認(rèn)的和構(gòu)造函數(shù)的原型關(guān)聯(lián);
當(dāng)使用一個(gè)方法進(jìn)行屬性或者方法訪問的時(shí)候,會(huì)先在當(dāng)前對(duì)象內(nèi)查找該屬性和方法,如果當(dāng)前對(duì)象內(nèi)未找到,就會(huì)去跟它關(guān)聯(lián)的原型對(duì)象內(nèi)進(jìn)行查找;
也就是說,在原型中定義的方法跟屬性,會(huì)被這個(gè)構(gòu)造函數(shù)創(chuàng)建出來的對(duì)象所共享;
訪問原型的方式:構(gòu)造函數(shù)名.prototype。
示例圖:
示例代碼: 給構(gòu)造函數(shù)的原型添加方法
function Dog(name,age){ this.name = name; this.age = age; } // 給構(gòu)造函數(shù)的原型 添加say方法 Dog.prototype.say = function(){ console.log("汪汪汪"); } var dog1 = new Dog("哈士奇", 1.5); var dog2 = new Dog("大黃狗", 0.5); dog1.say(); // 汪汪汪 dog2.say(); // 汪汪汪我們可以看到,本身Dog這個(gè)構(gòu)造函數(shù)中是沒有say這個(gè)方法的,我們通過Dog.prototype.say的方式,在構(gòu)造函數(shù)Dog的原型中創(chuàng)建了一個(gè)方法,實(shí)例化出來的dog1、dog2會(huì)先在自己的對(duì)象先找say方法,找不到的時(shí)候,會(huì)去他們的原型對(duì)象中查找。
如圖所示:
在構(gòu)造函數(shù)的原型中可以存放所有對(duì)象共享的數(shù)據(jù),這樣可以避免多次創(chuàng)建對(duì)象浪費(fèi)內(nèi)存空間的問題。
3.3 原型的使用1、使用對(duì)象的動(dòng)態(tài)特性
使用對(duì)象的動(dòng)態(tài)屬性,其實(shí)就是直接使用prototype為原型添加屬性或者方法。function Person () {} Person.prototype.say = function () { console.log( "講了一句話" ); }; Person.prototype.age = 18; var p = new Person(); p.say(); // 講了一句話 console.log(p.age); // 182、直接替換原型對(duì)象
每次構(gòu)造函數(shù)創(chuàng)建出來的時(shí)候,都會(huì)關(guān)聯(lián)一個(gè)空對(duì)象,我們可以用一個(gè)對(duì)象替換掉這個(gè)空對(duì)象。function Person () {} Person.prototype = { say : function () { console.log( "講了一句話" ); }, }; var p = new Person(); p.say(); // 講了一句話注意:
使用原型的時(shí)候,有幾個(gè)注意點(diǎn)需要注意一下,我們通過幾個(gè)案例來了解一下。使用對(duì)象.屬性名去獲取對(duì)象屬性的時(shí)候,會(huì)先在自身中進(jìn)行查找,如果沒有,就去原型中查找;
// 創(chuàng)建一個(gè)英雄的構(gòu)造函數(shù) 它有自己的 name 和 age 屬性 function Hero(){ this.name="德瑪西亞之力"; this.age=18; } // 給這個(gè)構(gòu)造函數(shù)的原型對(duì)象添加方法和屬性 Hero.prototype.age= 30; Hero.prototype.say=function(){ console.log("人在塔在?。。?); } var h1 = new Hero(); h1.say(); // 先去自身中找 say 方法,沒有再去原型中查找 打?。?人在塔在!?。? console.log(p1.name); // "德瑪西亞之力" console.log(p1.age); // 18 先去自身中找 age 屬性,有的話就不去原型中找了使用對(duì)象.屬性名去設(shè)置對(duì)象屬性的時(shí)候,只會(huì)在自身進(jìn)行查找,如果有,就修改,如果沒有,就添加;
// 創(chuàng)建一個(gè)英雄的構(gòu)造函數(shù) function Hero(){ this.name="德瑪西亞之力"; } // 給這個(gè)構(gòu)造函數(shù)的原型對(duì)象添加方法和屬性 Hero.prototype.age = 18; var h1 = new Hero(); console.log(h1); // {name:"德瑪西亞之力"} console.log(h1.age); // 18 h1.age = 30; // 設(shè)置的時(shí)候只會(huì)在自身中操作,如果有,就修改,如果沒有,就添加 不會(huì)去原型中操作 console.log(h1); // {name:"德瑪西亞之力",age:30} console.log(h1.age); // 30一般情況下,不會(huì)將屬性放在原型中,只會(huì)將方法放在原型中;
在替換原型的時(shí)候,替換之前創(chuàng)建的對(duì)象,和替換之后創(chuàng)建的對(duì)象的原型不一致?。?!
// 創(chuàng)建一個(gè)英雄的構(gòu)造函數(shù) 它有自己的 name 屬性 function Hero(){ this.name="德瑪西亞之力"; } // 給這個(gè)構(gòu)造函數(shù)的默認(rèn)原型對(duì)象添加 say 方法 Hero.prototype.say = function(){ console.log("人在塔在?。。?); } var h1 = new Hero(); console.log(h1); // {name:"德瑪西亞之力"} h1.say(); // "人在塔在?。?!" // 開辟一個(gè)命名空間 obj,里面有個(gè) kill 方法 var obj = { kill : function(){ console.log("大寶劍"); } } // 將創(chuàng)建的 obj 對(duì)象替換原本的原型對(duì)象 Hero.prototype = obj; var h2 = new Hero(); h1.say(); // "人在塔在?。?!" h2.say(); // 報(bào)錯(cuò) h1.kill(); // 報(bào)錯(cuò) h2.kill(); // "大寶劍"畫個(gè)圖理解下:
圖中可以看出,實(shí)例出來的h1對(duì)象指向的原型中,只有say()方法,并沒有kill()方法,所以h1.kill()會(huì)報(bào)錯(cuò)。同理,h2.say()也會(huì)報(bào)錯(cuò)。
3.4 __proto__屬性在js中以_開頭的屬性名為js的私有屬性,以__開頭的屬性名為非標(biāo)準(zhǔn)屬性。__proto__是一個(gè)非標(biāo)準(zhǔn)屬性,最早由firefox提出來。1、構(gòu)造函數(shù)的 prototype 屬性
之前我們?cè)L問構(gòu)造函數(shù)原型對(duì)象的時(shí)候,使用的是prototype屬性:function Person(){} //通過構(gòu)造函數(shù)的原型屬性prototype可以直接訪問原型 Person.prototype;在之前我們是無法通過構(gòu)造函數(shù)new出來的對(duì)象訪問原型的:function Person(){} var p = new Person(); //以前不能直接通過p來訪問原型對(duì)象2、實(shí)例對(duì)象的 __proto__ 屬性
__proto__屬性最早是火狐瀏覽器引入的,用以通過實(shí)例對(duì)象來訪問原型,這個(gè)屬性在早期是非標(biāo)準(zhǔn)的屬性,有了__proto__屬性,就可以通過構(gòu)造函數(shù)創(chuàng)建出來的對(duì)象直接訪問原型。function Person(){} var p = new Person(); //實(shí)例對(duì)象的__proto__屬性可以方便的訪問到原型對(duì)象 p.__proto__; //既然使用構(gòu)造函數(shù)的`prototype`和實(shí)例對(duì)象的`__proto__`屬性都可以訪問原型對(duì)象 //就有如下結(jié)論 p.__proto__ === Person.prototype;如圖所示:
3、__proto__屬性的用途
可以用來訪問原型;
在實(shí)際開發(fā)中除非有特殊的需求,不要輕易的使用實(shí)例對(duì)象的__proto__屬性去修改原型的屬性或方法;
在調(diào)試過程中,可以輕易的查看原型的成員;
由于兼容性問題,不推薦使用。
3.5 constuctor屬性constructor:構(gòu)造函數(shù),原型的constructor屬性指向的是和原型關(guān)聯(lián)的構(gòu)造函數(shù)。示例代碼:
function Dog(){ this.name="husky"; } var d=new Dog(); // 獲取構(gòu)造函數(shù) console.log(Dog.prototype.constructor); // 打印構(gòu)造函數(shù) Dog console.log(d.__proto__.constructor); // 打印構(gòu)造函數(shù) Dog如圖所示:
獲取復(fù)雜類型的數(shù)據(jù)類型:
通過obj.constructor.name的方式,獲取當(dāng)前對(duì)象obj的數(shù)據(jù)類型。在一個(gè)的函數(shù)中,有個(gè)返回值name,它表示的是當(dāng)前函數(shù)的函數(shù)名;
function Teacher(name,age){ this.name = name; this.age = age; } var teacher = new Teacher(); // 假使我們只知道一個(gè)對(duì)象teacher,如何獲取它的類型呢? console.log(teacher.__proto__.constructor.name); // Teacher console.log(teacher.constructor.name); // Teacher實(shí)例化出來的teacher對(duì)象,它的數(shù)據(jù)類型是啥呢?我們可以通過實(shí)例對(duì)象teacher.__proto__,訪問到它的原型對(duì)象,再通過.constructor訪問它的構(gòu)造函數(shù),通過.name獲取當(dāng)前函數(shù)的函數(shù)名,所以就能得到當(dāng)前對(duì)象的數(shù)據(jù)類型。又因?yàn)?b>.__proto__是一個(gè)非標(biāo)準(zhǔn)的屬性,而且實(shí)例出的對(duì)象繼承原型對(duì)象的方法,所以直接可以寫成:obj.constructor.name。
3.6 原型繼承原型繼承:每一個(gè)構(gòu)造函數(shù)都有prototype原型屬性,通過構(gòu)造函數(shù)創(chuàng)建出來的對(duì)象都繼承自該原型屬性。所以可以通過更改構(gòu)造函數(shù)的原型屬性來實(shí)現(xiàn)繼承。繼承的方式有多種,可以一個(gè)對(duì)象繼承另一個(gè)對(duì)象,也可以通過原型繼承的方式進(jìn)行繼承。
1、簡單混入繼承
直接遍歷一個(gè)對(duì)象,將所有的屬性和方法加到另一對(duì)象上。var animal = { name:"Animal", sex:"male", age:5, bark:function(){ console.log("Animal bark"); } }; var dog = {}; for (var k in animal){ dog[k]= animal[k]; } console.log(dog); // 打印的對(duì)象與animal一模一樣缺點(diǎn):只能一個(gè)對(duì)象繼承自另一個(gè)對(duì)象,代碼復(fù)用太低了。
2、混入式原型繼承
混入式原型繼承其實(shí)與上面的方法類似,只不過是將遍歷的對(duì)象添加到構(gòu)造函數(shù)的原型上。var obj={ name:"zs", age:19, sex:"male" } function Person(){ this.weight=50; } for(var k in obj){ // 將obj里面的所有屬性添加到 構(gòu)造函數(shù) Person 的原型中 Person.prototype[k] = obj[k]; } var p1=new Person(); var p2=new Person(); var p3=new Person(); console.log(p1.name); // "zs" console.log(p2.age); // 19 console.log(p3.sex); // "male"面向?qū)ο笏枷敕庋b一個(gè)原型繼承
我們可以利用面向?qū)ο蟮乃枷?,將面向過程進(jìn)行封裝。function Dog(){ this.type = "yellow Dog"; } // 給構(gòu)造函數(shù) Dog 添加一個(gè)方法 extend Dog.prototype.extend = function(obj){ // 使用混入式原型繼承,給 Dog 構(gòu)造函數(shù)的原型繼承 obj 的屬性和方法 for (var k in obj){ this[k]=obj[k]; } } // 調(diào)用 extend 方法 Dog.prototype.extend({ name:"二哈", age:"1.5", sex:"公", bark:function(){ console.log("汪汪汪"); } });3、替換式原型繼承
替換式原型繼承,在上面已經(jīng)舉過例子了,其實(shí)就是將一個(gè)構(gòu)造函數(shù)的原型對(duì)象替換成另一個(gè)對(duì)象。function Person(){ this.weight=50; } var obj={ name:"zs", age:19, sex:"male" } // 將一個(gè)構(gòu)造函數(shù)的原型對(duì)象替換成另一個(gè)對(duì)象 Person.prototype = obj; var p1=new Person(); var p2=new Person(); var p3=new Person(); console.log(p1.name); // "zs" console.log(p2.age); // 19 console.log(p3.sex); // "male"之前我們就說過,這樣做會(huì)產(chǎn)生一個(gè)問題,就是替換的對(duì)象會(huì)重新開辟一個(gè)新的空間。
替換式原型繼承時(shí)的bug
替換原型對(duì)象的方式會(huì)導(dǎo)致原型的constructor的丟失,constructor屬性是默認(rèn)原型對(duì)象指向構(gòu)造函數(shù)的,就算是替換了默認(rèn)原型對(duì)象,這個(gè)屬性依舊是默認(rèn)原型對(duì)象指向構(gòu)造函數(shù)的,所以新的原型對(duì)象是沒有這個(gè)屬性的。解決方法:手動(dòng)關(guān)聯(lián)一個(gè)constructor屬性
function Person() { this.weight = 50; } var obj = { name: "zs", age: 19, sex: "male" } // 在替換原型對(duì)象函數(shù)之前 給需要替換的對(duì)象添加一個(gè) constructor 屬性 指向原本的構(gòu)造函數(shù) obj.constructor = Person; // 將一個(gè)構(gòu)造函數(shù)的原型對(duì)象替換成另一個(gè)對(duì)象 Person.prototype = obj; var p1 = new Person(); console.log(p1.__proto__.constructor === Person); // true4、Object.create()方法實(shí)現(xiàn)原型繼承
當(dāng)我們想把對(duì)象1作為對(duì)象2的原型的時(shí)候,就可以實(shí)現(xiàn)對(duì)象2繼承對(duì)象1。前面我們了解了一個(gè)屬性:__proto__,實(shí)例出來的對(duì)象可以通過這個(gè)屬性訪問到它的原型,但是這個(gè)屬性只適合開發(fā)調(diào)試時(shí)使用,并不能直接去替換原型對(duì)象。所以這里介紹一個(gè)新的方法:Object.create()。語法: var obj1 = Object.create(原型對(duì)象);
示例代碼: 讓空對(duì)象obj1繼承對(duì)象obj的屬性和方法
var obj = { name : "蓋倫", age : 25, skill : function(){ console.log("大寶劍"); } } // 這個(gè)方法會(huì)幫我們創(chuàng)建一個(gè)原型是 obj 的對(duì)象 var obj1 = Object.create(obj); console.log(obj1.name); // "蓋倫" obj1.skill(); // "大寶劍"兼容性:
由于這個(gè)屬性是ECMAScript5的時(shí)候提出來的,所以存在兼容性問題。利用瀏覽器的能力檢測,如果存在Object.create則使用,如果不存在的話,就創(chuàng)建構(gòu)造函數(shù)來實(shí)現(xiàn)原型繼承。
// 封裝一個(gè)能力檢測函數(shù) function create(obj){ // 判斷,如果瀏覽器有 Object.create 方法的時(shí)候 if(Object.create){ return Object.create(obj); }else{ // 創(chuàng)建構(gòu)造函數(shù) Fun function Fun(){}; Fun.prototype = obj; return new Fun(); } } var hero = { name: "蓋倫", age: 25, skill: function () { console.log("大寶劍"); } } var hero1 = create(hero); console.log(hero1.name); // "蓋倫" console.log(hero1.__proto__ == hero); // true4.原型鏈對(duì)象有原型,原型本身又是一個(gè)對(duì)象,所以原型也有原型,這樣就會(huì)形成一個(gè)鏈?zhǔn)浇Y(jié)構(gòu)的原型鏈。4.1 什么是原型鏈示例代碼: 原型繼承練習(xí)
// 創(chuàng)建一個(gè) Animal 構(gòu)造函數(shù) function Animal() { this.weight = 50; this.eat = function() { console.log("蜂蜜蜂蜜"); } } // 實(shí)例化一個(gè) animal 對(duì)象 var animal = new Animal(); // 創(chuàng)建一個(gè) Preson 構(gòu)造函數(shù) function Person() { this.name = "zs"; this.tool = function() { console.log("菜刀"); } } // 讓 Person 繼承 animal (替換原型對(duì)象) Person.prototype = animal; // 實(shí)例化一個(gè) p 對(duì)象 var p = new Person(); // 創(chuàng)建一個(gè) Student 構(gòu)造函數(shù) function Student() { this.score = 100; this.clickCode = function() { console.log("啪啪啪"); } } // 讓 Student 繼承 p (替換原型對(duì)象) Student.prototype = p; //實(shí)例化一個(gè) student 對(duì)象 var student = new Student(); console.log(student); // 打印 {score:100,clickCode:fn} // 因?yàn)槭且患?jí)級(jí)繼承下來的 所以最上層的 Animate 里的屬性也是被繼承的 console.log(student.weight); // 50 student.eat(); // 蜂蜜蜂蜜 student.tool(); // 菜刀如圖所示:
我們將上面的案例通過畫圖的方式展現(xiàn)出來后就一目了然了,實(shí)例對(duì)象animal直接替換了構(gòu)造函數(shù)Person的原型,以此類推,這樣就會(huì)形成一個(gè)鏈?zhǔn)浇Y(jié)構(gòu)的原型鏈。完整的原型鏈
結(jié)合上圖,我們發(fā)現(xiàn),最初的構(gòu)造函數(shù)Animal創(chuàng)建的同時(shí),會(huì)創(chuàng)建出一個(gè)原型,此時(shí)的原型是一個(gè)空的對(duì)象。結(jié)合原型鏈的概念:“原型本身又是一個(gè)對(duì)象,所以原型也有原型”,那么這個(gè)空對(duì)象往上還能找出它的原型或者構(gòu)造函數(shù)嗎?我們?nèi)绾蝿?chuàng)建一個(gè)空對(duì)象? 1、字面量:{};2、構(gòu)造函數(shù):new Object()。我們可以簡單的理解為,這個(gè)空的對(duì)象就是,構(gòu)造函數(shù)Object的實(shí)例對(duì)象。所以,這個(gè)空對(duì)象往上面找是能找到它的原型和構(gòu)造函數(shù)的。
// 創(chuàng)建一個(gè) Animal 構(gòu)造函數(shù) function Animal() { this.weight = 50; this.eat = function() { console.log("蜂蜜蜂蜜"); } } // 實(shí)例化一個(gè) animal 對(duì)象 var animal = new Animal(); console.log(animal.__proto__); // {} console.log(animal.__proto__.__proto__); // {} console.log(animal.__proto__.__proto__.constructor); // function Object(){} console.log(animal.__proto__.__proto__.__proto__); // null如圖所示:
4.2 原型鏈的拓展1、描述出數(shù)組“[]”的原型鏈結(jié)構(gòu)
// 創(chuàng)建一個(gè)數(shù)組 var arr = new Array(); // 我們可以看到這個(gè)數(shù)組是構(gòu)造函數(shù) Array 的實(shí)例對(duì)象,所以他的原型應(yīng)該是: console.log(Array.prototype); // 打印出來還是一個(gè)空數(shù)組 // 我們可以繼續(xù)往上找 console.log(Array.prototype.__proto__); // 空對(duì)象 // 繼續(xù) console.log(Array.prototype.__proto__.__proto__) // null如圖所示:
2、擴(kuò)展內(nèi)置對(duì)象
給js原有的內(nèi)置對(duì)象,添加新的功能。注意:這里不能直接給內(nèi)置對(duì)象的原型添加方法,因?yàn)樵陂_發(fā)的時(shí)候,大家都會(huì)使用到這些內(nèi)置對(duì)象,假如大家都是給內(nèi)置對(duì)象的原型添加方法,就會(huì)出現(xiàn)問題。
錯(cuò)誤的做法:
// 第一個(gè)開發(fā)人員給 Array 原型添加了一個(gè) say 方法 Array.prototype.say = function(){ console.log("哈哈哈"); } // 第二個(gè)開發(fā)人員也給 Array 原型添加了一個(gè) say 方法 Array.prototype.say = function(){ console.log("啪啪啪"); } var arr = new Array(); arr.say(); // 打印 “啪啪啪” 前面寫的會(huì)被覆蓋為了避免出現(xiàn)這樣的問題,只需自己定義一個(gè)構(gòu)造函數(shù),并且讓這個(gè)構(gòu)造函數(shù)繼承數(shù)組的方法即可,再去添加新的方法。
// 創(chuàng)建一個(gè)數(shù)組對(duì)象 這個(gè)數(shù)組對(duì)象繼承了所有數(shù)組中的方法 var arr = new Array(); // 創(chuàng)建一個(gè)屬于自己的構(gòu)造函數(shù) function MyArray(){} // 只需要將自己創(chuàng)建的構(gòu)造函數(shù)的原型替換成 數(shù)組對(duì)象,就能繼承數(shù)組的所有方法 MyArray.prototype = arr; // 現(xiàn)在可以多帶帶的給自己創(chuàng)建的構(gòu)造函數(shù)的原型添加自己的方法 MyArray.prototype.say = function(){ console.log("這是我自己添加的say方法"); } var arr1 = new MyArray(); arr1.push(1); // 創(chuàng)建的 arr1 對(duì)象可以使用數(shù)組的方法 arr1.say(); // 也可以使用自己添加的方法 打印“這是我自己添加的say方法” console.log(arr1); // [1]4.3 屬性的搜索原則當(dāng)通過對(duì)象名.屬性名獲取屬性時(shí),會(huì)遵循以下屬性搜索的原則:1-首先去對(duì)象自身屬性中找,如果找到直接使用,
2-如果沒找到,去自己的原型中找,如果找到直接使用,
3-如果沒找到,去原型的原型中繼續(xù)找,找到直接使用,
4-如果沒有會(huì)沿著原型不斷向上查找,直到找到null為止。
5.Object.prototype成員介紹我們可以看到所有的原型最終都會(huì)繼承Object的原型:Object.prototype。打印看看Object的原型里面有什么:
// Object的原型 console.log(Object.prototype)如圖所示:
我們可以看到Object的原型里有很多方法,下面就來介紹下這些方法的作用。
5.1 constructor 屬性指向了和原型相關(guān)的構(gòu)造函數(shù)5.2 hasOwnProperty 方法判斷對(duì)象自身是否擁有某個(gè)屬性,返回值:布爾類型。示例代碼:
function Hero() { this.name = "蓋倫"; this.age = "25"; this.skill = function () { console.log("蓋倫使用了大寶劍"); } } var hero = new Hero(); console.log(hero.name); // "蓋倫" hero.skill(); // "蓋倫使用了大寶劍" console.log(hero.hasOwnProperty("name")); // true console.log(hero.hasOwnProperty("age")); // true console.log(hero.hasOwnProperty("skill")); // true console.log(hero.hasOwnProperty("toString")); // false toString是在原型鏈當(dāng)中的方法,并不是這里對(duì)象的方法 console.log("toString" in hero); // true in方法 判斷對(duì)象自身或者原型鏈中是否有某個(gè)屬性5.3 isPrototypeOf 方法對(duì)象1.isPrototypeOf(對(duì)象2),判斷對(duì)象1是否是對(duì)象2的原型,或者對(duì)象1是否是對(duì)象2原型鏈上的原型。示例代碼:
var obj = { age: 18 } var obj1 = {}; // 創(chuàng)建一個(gè)構(gòu)造函數(shù) function Hero() { this.name = "蓋倫"; } // 將這個(gè)構(gòu)造函數(shù)的原型替換成 obj Hero.prototype = obj; // 實(shí)例化一個(gè) hero 對(duì)象 var hero = new Hero(); console.log(obj.isPrototypeOf(hero)); // true 判斷 obj 是否是 hero 的原型 console.log(obj1.isPrototypeOf(hero)); // false 判斷 obj1 是否是 hero 的原型 console.log(Object.prototype.isPrototypeOf(hero)); // true 判斷 Object.prototype 是否是 hero 的原型 // 注意 這里的 Object.prototype 是原型鏈上最上層的原型對(duì)象5.4 propertyIsEnumerable 方法對(duì)象.propertyIsEnumerable("屬性或方法名"),判斷一個(gè)對(duì)象是否有該屬性,并且這個(gè)屬性可以被for-in遍歷,返回值:布爾類型。示例代碼:
// 創(chuàng)建一個(gè)構(gòu)造函數(shù) function Hero (){ this.name = "蓋倫"; this.age = 25; this.skill = function(){ console.log("蓋倫使用了大寶劍"); } } // 創(chuàng)建一個(gè)對(duì)象 var hero = new Hero(); // for-in 遍歷這個(gè)對(duì)象 我們可以看到分別打印了哪些屬性和方法 for(var k in hero){ console.log(k + "—" + hero[k]); // "name-蓋倫" "age-25" "skill-fn()" } // 判斷一個(gè)對(duì)象是否有該屬性,并且這個(gè)屬性可以被 for-in 遍歷 console.log(hero.propertyIsEnumerable("name")); // true console.log(hero.propertyIsEnumerable("age")); // true console.log(hero.propertyIsEnumerable("test")); // false5.5 toString 和 toLocalString 方法兩種方法都是將對(duì)象轉(zhuǎn)成字符串的,只不過toLocalString是按照本地格式進(jìn)行轉(zhuǎn)換。示例代碼:
// 舉個(gè)例子,時(shí)間的格式可以分為世界時(shí)間的格式和電腦本地的時(shí)間格式 var date = new Date(); // 直接將創(chuàng)建的時(shí)間對(duì)象轉(zhuǎn)換成字符串 console.log(date.toString()); // 將創(chuàng)建的時(shí)間對(duì)象按照本地格式進(jìn)行轉(zhuǎn)換 console.log(date.toLocaleString());效果圖:
5.6 valueOf 方法返回指定對(duì)象的原始值。MDN官方文檔
6.靜態(tài)方法和實(shí)例方法靜態(tài)方法和實(shí)例方法這兩個(gè)概念其實(shí)也是從面相對(duì)象的編程語言中引入的,對(duì)應(yīng)到JavaScript中的理解為:靜態(tài)方法: 由構(gòu)造函數(shù)調(diào)用的
在js中,我們知道有個(gè)Math構(gòu)造函數(shù),他有一個(gè)Math.abs()的方法,這個(gè)方法由構(gòu)造函數(shù)調(diào)用,所以就是靜態(tài)方法。Math.abs();實(shí)例方法: 由構(gòu)造函數(shù)創(chuàng)建出來的對(duì)象調(diào)用的
var arr = new Array(); // 由構(gòu)造函數(shù) Array 實(shí)例化出來的對(duì)象 arr 調(diào)用的 push 方法,叫做實(shí)例方法 arr.push(1);示例代碼:
function Hero(){ this.name="亞索"; this.say=function(){ console.log("哈撒ki"); } } Hero.prototype.skill=function(){ console.log("吹風(fēng)"); } // 直接給構(gòu)造函數(shù)添加一個(gè) run 方法(函數(shù)也是對(duì)象,可以直接給它加個(gè)方法) Hero.run=function(){ console.log("死亡如風(fēng),常伴吾身"); } var hero = new Hero(); hero.say(); hero.skill(); //實(shí)例方法 Hero.run(); //靜態(tài)方法如果這個(gè)方法是對(duì)象所有的,用實(shí)例方法。一般的工具函數(shù),用靜態(tài)方法,直接給構(gòu)造函數(shù)添加方法,不需要實(shí)例化,通過構(gòu)造函數(shù)名直接使用即可;
7.作用域“域”,表示的是一個(gè)范圍,“作用域”就是作用范圍。作用域說明的是一個(gè)變量可以在什么地方被使用,什么地方不能被使用。7.1 塊級(jí)作用域在ES5及ES5之前,js中是沒有塊級(jí)作用域的。{ var num = 123; { console.log( num ); // 123 } } console.log( num ); // 123上面這段代碼在JavaScript中是不會(huì)報(bào)錯(cuò)的,但是在其他的編程語言中(C#、C、JAVA)會(huì)報(bào)錯(cuò)。這是因?yàn)?,?b>JavaScript中沒有塊級(jí)作用域,使用{}標(biāo)記出來的代碼塊中聲明的變量num,是可以被{}外面訪問到的。但是在其他的編程語言中,有塊級(jí)作用域,那么{}中聲明的變量num,是不能在代碼塊外部訪問的,所以報(bào)錯(cuò)。
注意:塊級(jí)作用域只在在ES5及ES5之前不起作用,但是在ES6開始,js中是存在塊級(jí)作用域的。
7.2 詞法作用域詞法( 代碼 )作用域,就是代碼在編寫過程中體現(xiàn)出來的作用范圍。代碼一旦寫好,不用執(zhí)行,作用范圍就已經(jīng)確定好了,這個(gè)就是所謂詞法作用域。在js中詞法作用域規(guī)則:
函數(shù)允許訪問函數(shù)外的數(shù)據(jù);
整個(gè)代碼結(jié)構(gòu)中只有函數(shù)可以限定作用域;
作用域規(guī)則首先使用提升規(guī)則分析;
如果當(dāng)前作用規(guī)則中有名字了,就不考慮外面的名字。
作用域練習(xí):
第一題
var num=250; function test(){ // 會(huì)現(xiàn)在函數(shù)內(nèi)部查找有沒有這個(gè)num變量,有的話調(diào)用,沒有的話會(huì)去全局中查找,有就返回,沒有就返回undefined console.log(num); // 打印 250 } function test1(){ var num=222; test(); } test1();第二題
if(false){ var num = 123; } console.log(num); // undefined // {}是沒有作用域的 但是有判斷條件,var num會(huì)提升到判斷語句外部 所以不會(huì)報(bào)錯(cuò) 打印的是undefined第三題
var num = 123; function foo() { var num = 456; function func() { console.log( num ); } func(); } foo(); // 456 // 調(diào)用foo時(shí),在函數(shù)內(nèi)部調(diào)用了func,打印num的時(shí)候,會(huì)先在func中查找num 沒有的時(shí)候會(huì)去外層作用域找,找到即返回,找不到即再往上找。第四題
var num1 = 123; function foo1() { var num1 = 456; function foo2() { num1 = 789; function foo3 () { console.log( num1 ); // 789 自己的函數(shù)作用域中沒有就一層層往上找 } foo3(); } foo2(); } foo1(); console.log( num1 ); // 1237.3 變量提升(預(yù)解析)JavaScript是解釋型的語言,但是它并不是真的在運(yùn)行的時(shí)候逐句的往下解析執(zhí)行。我們來看下面這個(gè)例子:
func(); function func(){ alert("函數(shù)被調(diào)用了"); }在上面這段代碼中,函數(shù)func的調(diào)用是在其聲明之前,如果說JavaScript代碼真的是逐句的解析執(zhí)行,那么在第一句調(diào)用的時(shí)候就會(huì)出錯(cuò),然而事實(shí)并非如此,上面的代碼可以正常執(zhí)行,并且alert出來"函數(shù)被調(diào)用了"。
所以,可以得出結(jié)論,JavaScript并非僅在運(yùn)行時(shí)簡簡單單的逐句解析執(zhí)行!
JavaScript預(yù)解析
JavaScript引擎在對(duì)JavaScript代碼進(jìn)行解釋執(zhí)行之前,會(huì)對(duì)JavaScript代碼進(jìn)行預(yù)解析,在預(yù)解析階段,會(huì)將以關(guān)鍵字var和function開頭的語句塊提前進(jìn)行處理。關(guān)鍵問題是怎么處理呢?
當(dāng)變量和函數(shù)的聲明處在作用域比較靠后的位置的時(shí)候,變量和函數(shù)的聲明會(huì)被提升到當(dāng)前作用域的開頭。
示例代碼:函數(shù)名提升
正常函數(shù)書寫方式
function func(){ alert("函數(shù)被調(diào)用了"); } func();預(yù)解析之后,函數(shù)名提升
func(); function func(){ alert("函數(shù)被調(diào)用了"); }示例代碼:變量名提升
正常變量書寫方式
alert(a); // undefined var a = 123; // 由于JavaScript的預(yù)解析機(jī)制,上面這段代碼,alert出來的值是undefined, // 如果沒有預(yù)解析,代碼應(yīng)該會(huì)直接報(bào)錯(cuò)a is not defined,而不是輸出值。不是說要提前的嗎?那不是應(yīng)該alert出來123,為什么是undefined?
// 變量的時(shí)候 提升的只是變量聲明的提升,并不包括賦值 var a; // 這里是聲明 alert(a); // 變量聲明之后并未有初始化和賦值操作,所以這里是 undefined a = 123; // 這里是賦值注意:特殊情況
1、函數(shù)不能被提升的情況
函數(shù)表達(dá)式創(chuàng)建的函數(shù)不會(huì)提升
test(); // 報(bào)錯(cuò) "test is not a function" var test = function(){ console.log(123); }new Function創(chuàng)建的函數(shù)也不會(huì)被提升
test(); // 報(bào)錯(cuò) "test is not a function" var test = new Function(){ console.log(123); }2、出現(xiàn)同名函數(shù)
test(); // 打印 "好走的都是下坡路" // 兩個(gè)函數(shù)重名,這兩個(gè)函數(shù)都會(huì)被提升,但是后面的函數(shù)會(huì)覆蓋掉前面的函數(shù) function test(){ console.log("眾里尋她千百度,他正在自助烤肉...."); } function test(){ console.log("好走的都是下坡路"); }3、函數(shù)名與變量名同名
// 如果函數(shù)和變量重名,只會(huì)提升函數(shù),變量不會(huì)被提升 console.log(test); // 打印這個(gè)test函數(shù) function test(){ console.log("我是test"); } var test=200;再看一種情況:
var num = 1; function num () { console.log(num); // 報(bào)錯(cuò) “num is not a function” } num();直接上預(yù)解析后的代碼:
function num(){ console.log(num); } num = 1; num();4、條件式的函數(shù)聲明
// 如果是條件式的函數(shù)申明, 這個(gè)函數(shù)不會(huì)被預(yù)解析 test(); // test is not a function if(true){ function test(){ console.log("只是在人群中多看了我一眼,再也忘不掉我容顏..."); } }預(yù)解析是分作用域的
聲明提升并不是將所有的聲明都提升到window 對(duì)象下面,提升原則是提升到變量運(yùn)行的當(dāng)前作用域中去。示例代碼:
function showMsg(){ var msg = "This is message"; } alert(msg); // 報(bào)錯(cuò)“Uncaught ReferenceError: msg is not defined”預(yù)解析之后:
function showMsg(){ var msg; // 因?yàn)楹瘮?shù)本身就會(huì)產(chǎn)生一個(gè)作用域,所以變量聲明在提升的時(shí)候,只會(huì)提升在當(dāng)前作用域下最前面 msg = "This is message"; } alert(msg); // 報(bào)錯(cuò)“Uncaught ReferenceError: msg is not defined”預(yù)解析是分段的
分段,其實(shí)就分script標(biāo)簽的在上面代碼中,第一個(gè)script標(biāo)簽中的兩個(gè)func進(jìn)行了提升,第二個(gè)func覆蓋了第一個(gè)func,但是第二個(gè)script標(biāo)簽中的func并沒有覆蓋上面的第二個(gè)func。所以說預(yù)解析是分段的。
tip: 但是要注意,分段只是單純的針對(duì)函數(shù),變量并不會(huì)分段預(yù)解析。
函數(shù)預(yù)解析的時(shí)候是分段的,但是執(zhí)行的時(shí)候不分段
7.4 作用域鏈什么是作用域鏈?
只有函數(shù)可以制造作用域結(jié)構(gòu),那么只要是代碼,就至少有一個(gè)作用域, 即全局作用域。凡是代碼中有函數(shù),那么這個(gè)函數(shù)就構(gòu)成另一個(gè)作用域。如果函數(shù)中還有函數(shù),那么在這個(gè)作用域中就又可以誕生一個(gè)作用域。將這樣的所有的作用域列出來,可以有一個(gè)結(jié)構(gòu): 函數(shù)內(nèi)指向函數(shù)外的鏈?zhǔn)浇Y(jié)構(gòu)。就稱作作用域鏈。
例如:
function f1() { function f2() { } } var num = 456; function f3() { function f4() { } }示例代碼:
var num=200; function test(){ var num=100; function test1(){ var num=50; function test2(){ console.log(num); } test2(); } test1(); } test(); // 打印 “50”如圖所示:
繪制作用域鏈的步驟:
看整個(gè)全局是一條鏈, 即頂級(jí)鏈, 記為0級(jí)鏈
看全局作用域中, 有什么變量和函數(shù)聲明, 就以方格的形式繪制到0級(jí)練上
再找函數(shù), 只有函數(shù)可以限制作用域, 因此從函數(shù)中引入新鏈, 標(biāo)記為1級(jí)鏈
然后在每一個(gè)1級(jí)鏈中再次往復(fù)剛才的行為
變量的訪問規(guī)則:
首先看變量在第幾條鏈上, 在該鏈上看是否有變量的定義與賦值, 如果有直接使用
如果沒有到上一級(jí)鏈上找( n - 1 級(jí)鏈 ), 如果有直接用, 停止繼續(xù)查找.
如果還沒有再次往上剛找... 直到全局鏈( 0 級(jí) ), 還沒有就是 is not defined
注意,同級(jí)的鏈不可混合查找
來點(diǎn)案例練練手
第一題:
function foo() { var num = 123; console.log(num); //123 } foo(); console.log(num); // 報(bào)錯(cuò)第二題:
var scope = "global"; function foo() { console.log(scope); // undefined var scope = "local"; console.log(scope); // "local" } foo(); // 預(yù)解析之后 // var scope = "global"; // function foo() { // var scope; // console.log(scope); // undefined // scope = "local"; // console.log(scope); // local // }第三題:
if("a" in window){ var a = 10; } console.log(a); // 10 // 預(yù)解析之后 // var a; // if("a" in window){ // a = 10; // 判斷語句不產(chǎn)生作用域 // } // console.log(a); // 10第四題:
if(!"a" in window){ var a = 10; } console.log(a); // undefined // 預(yù)解析之后 // var a; // if(!"a" in window){ // a = 10; // 判斷語句不產(chǎn)生作用域 // } // console.log(a); // undefined第五題
// console.log(num); 報(bào)錯(cuò) 雖然num是全局變量 但是不會(huì)提升 function test(){ num = 100; } test(); console.log(num); // 100第六題
var foo = 1; function bar() { if(!foo) { var foo = 10; } console.log(foo); // 10 } bar(); // 預(yù)解析之后 // var foo=1; // function bar(){ // var foo; // if(!foo){ // foo=10; // } // console.log(foo); // 10 // } // bar();8.FunctionFunction是函數(shù)的構(gòu)造函數(shù),你可能會(huì)有點(diǎn)蒙圈,沒錯(cuò),在js中函數(shù)與普通的對(duì)象一樣,也是一個(gè)對(duì)象類型,只不過函數(shù)是js中的“一等公民”。這里的Function類似于Array、Object等
8.1 創(chuàng)建函數(shù)的幾種方式1、函數(shù)字面量(直接聲明函數(shù))創(chuàng)建方式
function test(){ // 函數(shù)體 } // 類似于對(duì)象字面量創(chuàng)建方式:{}2、函數(shù)表達(dá)式
var test = function(){ // 函數(shù)體 }3、Function構(gòu)造函數(shù)創(chuàng)建
// 構(gòu)造函數(shù)創(chuàng)建一個(gè)空的函數(shù) var fn = new Function(); fn1(); // 調(diào)用函數(shù)函數(shù)擴(kuò)展名
有沒有一種可能,函數(shù)表達(dá)式聲明函數(shù)時(shí),function 也跟著一個(gè)函數(shù)名,如:var fn = function fn1(){}? 答案是可以的,不過fn1只能在函數(shù)內(nèi)部使用,并不能在外部調(diào)用。var fn = function fn1(a,b,c,d){ console.log("當(dāng)前函數(shù)被調(diào)用了"); // 但是,fn1可以在函數(shù)的內(nèi)部使用 console.log(fn1.name); console.log(fn1.length); // fn1(); 注意,這樣調(diào)用會(huì)引起遞歸!?。? 下面我們會(huì)講到什么是遞歸。 } // fn1(); // 報(bào)錯(cuò),fn1是不能在函數(shù)外部調(diào)用的 fn(); // "當(dāng)前函數(shù)被調(diào)用了" // 函數(shù)內(nèi)部使用時(shí)打?。?// "當(dāng)前函數(shù)被調(diào)用了" // console.log(fn1.name); => "fn1" // console.log(fn1.length); => 48.2 Function 構(gòu)造函數(shù)創(chuàng)建函數(shù)上面我們知道了如何通過Function構(gòu)造函數(shù)創(chuàng)建一個(gè)空的函數(shù),這里我們對(duì)它的傳參詳細(xì)的說明下。1、不傳參數(shù)時(shí)
// 不傳參數(shù)時(shí),創(chuàng)建的是一個(gè)空的函數(shù) var fn1 = new Function(); fn1(); // 調(diào)用函數(shù)2、只傳一個(gè)參數(shù)
// 只傳一個(gè)參數(shù)的時(shí)候,這個(gè)參數(shù)就是函數(shù)體 // 語法:var fn = new Function(函數(shù)體); var fn2 = new Function("console.log(2+5)"); f2(); // 73、傳多個(gè)參數(shù)
// 傳多個(gè)參數(shù)的時(shí)候,最后一個(gè)參數(shù)為函數(shù)體,前面的參數(shù)都是函數(shù)的形參名 // 語法:var fn = new Function(arg1,arg2,arg3.....argn,metthodBody); var fn3 = new Function("num1","num2","console.log(num1+num2)"); f3(5,2); // 78.3 Function 的使用1、用Function創(chuàng)建函數(shù)的方式封裝一個(gè)計(jì)算m - n之間所有數(shù)字的和的函數(shù)
//求 m-n之間所有數(shù)字的和 //var sum=0; //for (var i = m; i <=n; i++) { // sum+=i; //} var fn = new Function("m","n","var sum=0;for (var i = m; i <=n; i++) {sum+=i;} console.log(sum);"); fn(1,100); // 5050函數(shù)體參數(shù)過長問題:
函數(shù)體過長時(shí),可讀性很差,所以介紹解決方法:1)字符串拼接符“+”
var fn = new Function( "m", "n", "var sum=0;"+ "for (var i = m; i <=n; i++) {"+ "sum += i;"+ "}"+ "console.log(sum);" ); fn(1,100); // 50502)ES6中新語法“ ` ”,(在esc鍵下面)
表示可換行字符串的界定符,之前我們用的是單引號(hào)或者雙引號(hào)來表示一個(gè)字符串字面量,在ES6中可以用反引號(hào)來表示該字符串可換行。new Function( "m", "n", `var sum=0; for (var i = m; i <=n; i++) { sum+=i; } console.log(sum);` );3)模板方式
2、eval 函數(shù)
eval函數(shù)可以直接將把字符串的內(nèi)容,作為js代碼執(zhí)行,前提是字符串代碼符合js代碼規(guī)范。這里主要是用作跟Function傳參比較。eval 和 Function 的區(qū)別:
Function();中,方法體是字符串,必須調(diào)用這個(gè)函數(shù)才能執(zhí)行
eval(); 可以直接執(zhí)行字符串中的js代碼
存在的問題:
性能問題
因?yàn)?b>eval里面的代碼是直接執(zhí)行的,所以當(dāng)在里面定義一個(gè)變量的時(shí)候,這個(gè)變量是不會(huì)預(yù)解析的,所以會(huì)影響性能。// eval 里面的代碼可以直接執(zhí)行,所以下面的打印的 num 可以訪問到它 // 但是這里定義的 num 是沒有預(yù)解析的,所以變量名不會(huì)提升,從而性能可能會(huì)變慢 eval("var num = 123;"); console.log(num); // 123安全問題
主要的安全問題是可能會(huì)被利用做XSS攻擊(跨站腳本攻擊(Cross Site Scripting)),eval也存在一個(gè)安全問題,因?yàn)樗梢詧?zhí)行傳給它的任何字符串,所以永遠(yuǎn)不要傳入字符串或者來歷不明和不受信任源的參數(shù)。示例代碼: 實(shí)現(xiàn)一個(gè)簡單的計(jì)算器
效果圖:
8.4 Function 的原型鏈結(jié)構(gòu)在7.2章節(jié)中我們知道函數(shù)也還可以通過構(gòu)造函數(shù)的方式創(chuàng)建出來,既然可以通過構(gòu)造函數(shù)的方式創(chuàng)建,那么函數(shù)本身也是有原型對(duì)象的。示例代碼:
// 通過Function構(gòu)造函數(shù)創(chuàng)建一個(gè)函數(shù)test var test = new Function(); // 既然是通過構(gòu)造函數(shù)創(chuàng)建的,那么這個(gè)函數(shù)就有指向的原型 console.log(test.__proto__); // 打印出來的原型是一個(gè)空的函數(shù) console.log(test.__proto__.__proto__); // 空的函數(shù)再往上找原型是一個(gè)空的對(duì)象 console.log(test.__proto__.__proto__.__proto__); // 再往上找就是null了 // 函數(shù)原型鏈: test() ---> Function.prototype ---> Object.prototype ---> null如圖所示:
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/100458.html
摘要:個(gè)人前端文章整理從最開始萌生寫文章的想法,到著手開始寫,再到現(xiàn)在已經(jīng)一年的時(shí)間了,由于工作比較忙,更新緩慢,后面還是會(huì)繼更新,現(xiàn)將已經(jīng)寫好的文章整理一個(gè)目錄,方便更多的小伙伴去學(xué)習(xí)。 showImg(https://segmentfault.com/img/remote/1460000017490740?w=1920&h=1080); 個(gè)人前端文章整理 從最開始萌生寫文章的想法,到著手...
摘要:前言月份開始出沒社區(qū),現(xiàn)在差不多月了,按照工作的說法,就是差不多過了三個(gè)月的試用期,準(zhǔn)備轉(zhuǎn)正了一般來說,差不多到了轉(zhuǎn)正的時(shí)候,會(huì)進(jìn)行總結(jié)或者分享會(huì)議那么今天我就把看過的一些學(xué)習(xí)資源主要是博客,博文推薦分享給大家。 1.前言 6月份開始出沒社區(qū),現(xiàn)在差不多9月了,按照工作的說法,就是差不多過了三個(gè)月的試用期,準(zhǔn)備轉(zhuǎn)正了!一般來說,差不多到了轉(zhuǎn)正的時(shí)候,會(huì)進(jìn)行總結(jié)或者分享會(huì)議!那么今天我就...
閱讀 1946·2021-11-23 09:51
閱讀 1252·2019-08-30 15:55
閱讀 1625·2019-08-30 15:44
閱讀 771·2019-08-30 14:11
閱讀 1152·2019-08-30 14:10
閱讀 922·2019-08-30 13:52
閱讀 2641·2019-08-30 12:50
閱讀 625·2019-08-29 15:04