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