摘要:一些額外的全局函數命名空間對象接口和構造函數與沒有典型的關聯,但卻是有效的。最后有幾點需要說明的是每個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針。
寫這篇文章的目的很簡單,就是想把之前一些不太清晰的概念梳理一下,網上這類教程很多,但是本文盡可能還原問題本質,注意知識點之間的聯系。相信看過我前面的博客的朋友一定知道我寫文章的風格了,盡可能詳盡,而且不是只是為了解決某一個小問題而寫,方便大家知識點更體系,一篇內容其實相當于一章節的內容,容量有點大,我也不是一天完成的,一般是一周時間左右,所以大家閱讀的話可能也需要一些時間才能有所收獲。作為進階教程,本文將簡要講述JavaScript面向對象編程的內容,但是不會介紹什么是接口,什么是對象,什么是對象屬性,什么是對象方法,但是相信你看完了后自然理所當然的理解了這些基本概念。
在開始學習之前我們首先用一個工具,就是瀏覽器自帶的開發者工具控制臺。我這里用hbuider直接打開這個工具,在【web瀏覽器】預覽工具欄右鍵單擊會彈出一個框框,在這個框框中選擇【Console】,然后在命令行輸入js代碼我們就可以看到執行結果,這里我們先輸入一個window ,然后會發現有json 結構的內容。
這么說相信新手應該沒啥感覺,最好還是舉個例子說說,比如我們去吃飯要點菜,Window說白了一個菜單,window是端上桌子的那道菜,至于這道菜色香味以及制作方法和Window無關,只和window有關。
window.document指向document對象的引用,document對象是Document 接口接口的具體實現。Document 接口代表在瀏覽器及服務器中加載任意 web 頁面,也作為 web 頁面內容(DOM tree, 包含如
雖然這段話看起來,但是實際上意思很簡單: 如果我們要想獲取一個document的內容,我們可以使用document對象下的方法屬性和方法去獲取,比如獲取標題:
hello world
我們這里的document為啥不加window也可以彈出結果呢,因為window為頂層對象,這里可以忽略不寫,比如alert() 方法其實是window.alert() 下的方法,我們這里不寫window,一樣可以得到結果。另外,我們這里只是獲取了title,至于其他的內容,那就要學習document的屬性和方法。
__proto__ 屬性(原型指針) 和 prototype屬性(原型對象)
說到這兩個屬性,我們真的很糾結,這兩者到底有什么聯系和區別呢?我們先看下面的例子:
> window.prototype === window.__proto__ // false
> Window.prototype === window.__proto__ // true
> window.constructor === Window // true
> Window.__proto__.__proto__.__proto__.__proto__ // null
臥槽,這是什么鬼?prototype 和__proto__ 到底分別各自指什么,Window鏈式調用__proto__ 怎么最后會變成null? 似乎說到這里謎團越來越多了,我們這里就要跳出window對象舉個簡單例子說說,不然大家真的是暈的。
function person(name) {
this.name = name;
this.getName = function() {
alert(this.name)
}
}
var zhangsan = new person("zhangsan");
var lisi = new person("lisi");
console.log(zhangsan.name)
console.log(lisi.name)
zhangsan.getName();
lisi.getName();
結果:
"zhangsan"
"lisi"
注:可以使用關鍵字 this調用類中的屬性, this是對當前對象的引用。
這樣一個例子我們似乎看到了面向對象中繼承的特性,在其他面向對象語言中,這里的person函數被設計為“類”,但是在JavaScript中這里設計得有點畸形的感覺,為啥這么說呢,因為這里的person是一個構造函數(constructor),用new實例化的也不是其他面向對象語言中的類,而是構造函數 ,這種設計導致一個問題是啥呢?無法共享屬性和方法,每一個實例對象,都有自己的屬性和方法的副本!??!
比如:每一個實例對象都有getName(),都是從父親構造器中繼承得到,這樣就產生多個副本,但是我們希望這個方法是公用的,避免多個副本的資源浪費,我們希望能夠把公用的屬性方法提取出來,然后實例化的對象也可以引用,但是不會直接拷貝一份作為副本。這個時候構造函數(constructor)顯得有點力不從心了,JavaScript的設計者引入了一個重要的屬性prototype,這個屬性包含一個對象(通常稱為“prototype對象")。我們把這個例子改成用prototype寫試試:
function person(name) {
this.name = name;
}
person.prototype.getName = function() {
alert(this.name)
}
var zhangsan = new person("zhangsan");
var lisi = new person("lisi");
這樣我們多個實例化對象可以公用同一個方法,換句話說所有的實例對象共享同一個prototype對象,通常稱為原型。一層層的繼承實現了鏈條式的"原型鏈"(prototype chain),JavaScript因此通過這個原型鏈實現繼承。至于為啥最開始怎么設計,都是為了開發者簡單,但是也因此給大家的感覺是特別,而且特別難理解,但是事實上其實并沒有那么神奇?。。?/p>
prototype屬性很特殊,它還有一個隱式的constructor,指向了構造函數本身。
> person.prototype.constructor === person // true
> zhangsan.constructor === person // true
> zhangsan.constructor === person.prototype.constructor // true
說了這個多,我們一直沒有解釋__proto__ 屬性,我們上面講了可以通過構造函數的prototype 屬性實現繼承共用公用的屬性方法,但是我們沒有說明實例化對象如何訪問到它所繼承的對象的原型對象,這里的__proto__ 屬性就是這個作用。我們再回過頭去看之前的問題: 因為window 是通過實例化Window 得到,自然我們訪問Window 原型對象有兩種方法:1.直接通過Window的prototype屬性;2.通過實例化子對象的__proto__ 訪問父對象的原型對象。這兩種方法實現的結果一模一樣。
Window.prototype === window.__proto__ // true
另外在JavaScript中有一個很特別的地方:萬物皆對象,萬物皆為空。 怎么理解呢,在JavaScript中的一切都源于對象,而且最頂層的對象是null對象,這會讓人很費解的。所以當我們通過__proto__ 不斷的尋找最頂層的原型對象時會發現為null。
基于原型的編程不是面向對象編程中體現的風格,且行為重用(在基于類的語言中也稱為繼承)是通過裝飾它作為原型的現有對象的過程實現的。這種模式也被稱為弱類化,原型化,或基于實例的編程。
最后有幾點需要說明的是:
每個構造函數都有一個原型對象(prototype),原型對象都包含一個指向構造函數的指針(constructor),而實例都包含一個指向原型對象的內部指針(__proto__ )。
除了使用__proto__ 方式訪問對象的原型,還可以通過Object.getPrototypeOf 方法來獲取對象的原型,以及通過Object.setPrototypeOf 方法來重寫對象的原型。__proto__ 屬性只有瀏覽器才需要部署,其他環境可以沒有這個屬性,而且前后的兩根下劃線,表示它本質是一個內部屬性,不應該對使用者暴露。
instanceof和Object.isPrototypeOf()可以判斷兩個對象是否是繼承關系。如上面那個例子:
// instanceof 運算符返回一個布爾值,表示一個對象是否由某個構造函數創建。
> zhangsan instanceof person
=> true
// Object.isPrototypeOf()只要某個對象處在原型鏈上,都返回true。
> person.prototype.isPrototypeOf(zhangsan)
=> true
這里推薦大家看看下面幾篇文章:
JavaScript面向對象簡介
JavaScript 原型中的哲學思想
Javascript繼承機制的設計思想
如何打造一個自己的類jQuery的js工具庫?
文章寫到本來是準備重新開篇的,剛剛上面在window下將原型鏈繼承不知道會不會有點誤導一些朋友,因為最開始是準備以window對象入手將面向對象的內容整理一下,發現寫著寫著有點零散了,因為window對象有很多其他內容值得將,但是篇幅和本文主題影響,只能先停下后面再開篇補充,講了原型鏈繼承的理論知識,我們自然要實際動手做點項目才能說明問題。
基本概念講解
如果我們去查看一些js庫的寫法,我們會發現經常有這樣一種結構:
(function(w,undefined) {
//...
})(window);
在理解為什么要這樣寫之前我們首先要明白什么JavaScript的作用域,什么是匿名函數,什么是閉包?
作用域
在es6之前,JavaScript是遵循函數作用域,不支持塊級作用域。
var i=0;
if(i<2){
var i = 2;
}
alert(i); // 2
在es6中支持使用let聲明了一個塊級域的本地變量,并且可以同時初始化該變量。
var i=0;
if(i<2){
let i = 2;
}
alert(i); // 0
函數內部可以直接讀取函數全局變量。函數內的變量如果是使用var 申明,則是局部變量,作用域范圍為函數體內部,不可讀?。坏切枰⒁獾氖俏唇涍^var申明,就變成了全局變量,在函數外部也可以調用。
// 局部變量類型:
var i=0;
var fn = function () {
if(i<2){
var i = 2;
}
}
fn();
alert(i); // 0
// 全局變量類型
var i=0;
var fn = function () {
if(i<2){
i = 2;
}
}
fn();
alert(i); // 2
變量提升:一個變量或函數可以在它被引用之后聲明。
【變量】
foo = 2
var foo;
// 被隱式地解釋為:
var foo;
foo = 2;
【函數】
hoisted(); // logs "foo"
function hoisted() {
console.log("foo");
}
匿名函數:沒有函數名稱的函數
匿名函數是這樣的:
function(arg1,arg2){
// code
}
但是通常我們會把匿名函數寫成自執行的匿名函數:
(function(arg1,arg2){
// code
})(a1,a2);
等價于:
var fn = function(arg1,arg2){
// code
}
fn(a1,a2);
其實這里就是實參與形參的關系,arg1,arg2在函數體內作為形參被引用,a1,a2作為實參在調用的時候傳入到函數體中被調用,至于變量內部存儲原理這里不做深入探究,畢竟學過編程的人應該都清楚。
我們現在回過頭來看看本小節開頭說的那個例子,為啥要那樣寫呢?
(function(w,undefined) {
//...
})(window);
為什么要傳入 window? 通過傳入 window變量,使得 window由全局變量變為局部變量,當在我們封裝的代碼塊中訪問 window時,不需要將作用域鏈回退到頂層作用域,這樣可以更快的訪問 window;同時將 window作為參數傳入,可以在壓縮代碼時進行優化。
為什么要傳入 undefined? 在只執行匿名函數的作用域內,確保 undefined 是真的未定義。因為 undefined 能夠被重寫,賦予新的值。
閉包
我們前面說了在函數外可以調用函數內未經過var聲明的全局變量,但是如何從外部讀取函數局部變量呢?我們可以在函數內部再定義一個函數。
var fn = function(){
var name = "local";
var f = function(){
alert(name);
}
return f
}
// 調用
var resurlt = fn();
resurlt();
// or
fn()();
閉包主要有兩個作用: 一是可以讀取函數內部的變量,另一個就是讓這些變量的值始終保持在內存中。
讀取函數內部變量我們很好理解,但是至于內部變量的值保存在存儲中這個就有點難理解,我們看個例子:
var fn = function(){
var i = 0;
add = function(){
i++;
}
var f = function(){
alert(i);
}
return f
}
var result = fn();
result(); // 0
add();
result(); // 1
add未加var 聲明是全局變量,如果變量i不在內存中存儲,那么我們第一次和第二次調用result() 值都應該為0。原因在于我們將fn()的返回值f()數賦值給一個全局變量,由于這個全局變量一直處于內存中,f函數同樣也在內存中,f()函數依賴于fn()函數,因此fn()中的局部變量i一直處于內存之中。 如果上面的例子在調用的時候使用fn()() 則不會出現這種情況。
1)由于閉包會使得函數中的變量都被保存在內存中,內存消耗很大,所以不能濫用閉包,否則會造成網頁的性能問題,在IE中可能導致內存泄露。解決方法是,在退出函數之前,將不使用的局部變量全部刪除。
2)閉包會在父函數外部,改變父函數內部變量的值。所以,如果你把父函數當作對象(object)使用,把閉包當作它的公用方法(Public Method),把內部變量當作它的私有屬性(private value),這時一定要小心,不要隨便改變父函數內部變量的值?!?學習Javascript閉包(Closure)
這里簡要講解了一下閉包的一些作用,主要是為了幫助我們理解為啥一些js庫采用閉包。
jQuery中鏈式調用的實現原理
首先我們怎么實現私有命名空間? 通過定義一個匿名函數,創建了一個"私有"的命名空間,該命名空間的變量和方法,不會破壞全局的命名空間,我們只暴漏出一個頂層的對象供外部調用即可。
前面我們講到window對象的時候有個知識點沒有說的是,我們在頁面定義一個全局變量的時候,這個全局變量最終是會在window對象下,對于調用window對象下的屬性和方法我們一般無需通過window. 的形式就可以調用。同理當我們引用jQuery這種庫的時候,jQuery對象會在引用頁面的window對象下,這是因為jQuery庫最后會將jQuery或者$對象掛在window對象下,這樣就實現了頂層對象的暴漏。
下面我們實現一個適用于現代瀏覽器的極小DOM操作庫,主要解決移動端,所以我們這里取名為mjs。
(function(w,undefined) {
// 構造函數
var mjs = function(selector, context) {
return new mjs.fn.init(selector, context);
}
// 構造函數mjs的原型對象
mjs.fn = mjs.prototype = {
constructor: mjs,
init: function (selector, context) {
//...
}
}
mjs.fn.init.prototype = mjs.fn;
// 為window全局變量添加mjs對象
w.mjs = w.m = mjs;
})(window);
這樣我們就可以無需new mjs(),直接使用 mjs.* 或者 m.* 鏈式調用相關方法。
selector(選擇器)
下面我們實現一個最簡的選擇器,這里我不考慮兼容低級版本瀏覽器,使用querySelectorAll實現。我們接著上面的完善mjs.prototype.init方法。我們如果不考慮鏈式調用,我們最簡單的選擇器甚至可以長這樣:
var $ = function (selector) {
return document.querySelector(selector);
}
調用:
$(".content")
如果想給選擇器加一個上下文,我們進一步可以這樣:
/**
* 選擇器
* @param {Object} selector
* @param {Object} context
*/
var $ = function (selector, context) {
var context = context || document;
var el = context.querySelectorAll(selector);
return Array.prototype.slice.call(el);
};
// 調用
var divObj = $(".div",$(".content")[0]);
console.log(divObj[0].innerHTML)
當然我們這里補充完整就是這樣了:
// 構造函數mjs的原型對象
mjs.prototype = {
constructor: mjs,
init: function (selector, context) {
if(!selector) {
return mjs;
}else if(typeof selector === "object"){
var selector = [selector];
for (var i = 0; i < selector.length; i++) {
this[i] = selector[i];
}
this.length = selector.length;
return mjs;
}else if(typeof selector === "string"){
var selector = selector.trim();
var context = context || document;
var el = context.querySelectorAll(selector);
var dom = Array.prototype.slice.call(el);
var length = dom.length;
for (var i = 0; i < length; i++) {
this[i] = dom[i];
}
this.context = context;
this.selector = selector;
this.length = length;
return this;
}
}
}
這里我們先只完成最簡單的選擇器功能,還有當selector是function類型的我們沒有進行判斷,這里不貼出來,大家具體可以看看源代碼。我們可以驗證一下我們封裝的這個選擇器:
div1
span1
console.log(mjs(".divBox")[0].innerHTML)
// "div1span1 "
console.log(mjs(".divBox span")[0].innerHTML)
// "span1"
var divBox = mjs(".divBox")[0];
console.log(mjs("span",divBox)[0].innerHTML)
// "span1"
因為innerHTML是原生DOM操作的方法,我們mjs對象沒有這個方法,所以我們這里是將mjs對象轉成了原生DOM對象,轉換方法:mjs(selector)[0]。
html()、text()、attr()
為了簡單起見我們繼續封裝,先完成一個html()方法。
...
html: function (content) {
if (content === undefined && this[0].nodeType === 1) {
return this[0].innerHTML.trim();
}else{
var len = this.length;
for (var i = 0; i < len; i++) {
this[i].innerHTML = content;
}
return this;
}
},
text: function (val) {
if (!arguments.length) {
return this[0].textContent.trim();
} else {
for (var i = 0; i < this.length; i++) {
this[i].innerText = val;
}
return this;
}
}
...
上面的例子我們可以這樣調用:
// 直接獲取內容,默認獲取第一個匹配項
console.log(mjs(".divBox").html())
console.log(mjs(".divBox span").html())
console.log(mjs(".divBox span").text())
// 更新內容,默認更新所有匹配項
mjs(".divBox span").html("我是新的內容")
mjs(".divBox span").text("我是新的內容")
// 支持上下文查找方法
console.log(mjs("span",mjs(".divBox")[0]).html())
// 設置屬性
mjs(".divBox").attr("name","divBox");
// 獲取屬性
console.log(mjs(".divBox").attr("name"))
prepend()、append()、before()、after()、remove()
prepend: function(str) {
var len = this.length;
for (var i = 0; i < len; i++) {
this[i].insertAdjacentHTML("afterbegin", str);
}
return this;
},
append: function (str) {
var len = this.length;
for (var i = 0; i < len; i++) {
this[i].insertAdjacentHTML("beforeend", str);
}
return this;
},
before: function (str) {
var len = this.length;
for (var i = 0; i < len; i++) {
this[i].insertAdjacentHTML("beforebegin", str);
}
return this;
},
after: function (str) {
var len = this.length;
for (var i = 0; i < len; i++) {
this[i].insertAdjacentHTML("afterend", str);
}
return this;
},
remove: function () {
var len = this.length;
for (var i = 0; i < len; i++) {
this[i].parentNode.removeChild(this[i]);
}
return this;
}
調用:
// 添加元素
mjs(".divBox").prepend(" ")
mjs(".divBox").append("")
mjs(".divBox").before("")
mjs(".divBox").after("")
// 刪除元素
mjs(".divBox").remove();
insertAdjacentHTML() 將指定的文本解析為 HTML 或 XML,然后將結果節點插入到 DOM 樹中的指定位置處。該方法不會重新解析調用該方法的元素,因此不會影響到元素內已存在的元素節點。從而可以避免額外的解析操作,比直接使用 innerHTML 方法要快?!狹DN insertAdjacentHTML
語法: element.insertAdjacentHTML(position, text); position 是相對于 element 元素的位置,并且只能是以下的字符串之一:
beforebegin: 在 element 元素的前面。
afterbegin:在 element 元素的第一個子節點前面。
beforeend:在 element 元素的最后一個子節點后面。
afterend:在 element 元素的后面。
hasClass()、addClass()、removeClass()、toggleClass()
...
hasClass: function (cls) {
return this[0].classList.contains(cls);
},
addClass: function (cls) {
var len = this.length;
for (var i = 0; i < len; i++) {
if(!this[i].classList.contains(cls)){
this[i].classList.add(cls);
}
}
return this;
},
removeClass: function (cls) {
var len = this.length;
for (var i = 0; i < len; i++) {
if(this[i].classList.contains(cls)){
this[i].classList.remove(cls);
}
}
return this;
},
toggleClass: function (cls) {
return this[0].classList.toggle(cls);
}
...
調用方法:
// hasClass(返回值為布爾值)
console.log(mjs(".divBox").hasClass("divBox"))
// addClass
mjs(".divBox").addClass("red")
// removeClass
mjs(".divBox").removeClass("red")
// toggleClass
mjs(".divBox").toggleClass("red")
css()
css: function (attr,val) {
var len = this.length;
for(var i = 0;i < len; i++) {
if(arguments.length === 1){
var obj = arguments[0];
if(typeof obj === "string"){
return getComputedStyle(this[i],null)[attr];
}else if(typeof obj === "object"){
for(var attr in obj){
this[i].style[attr] = obj[attr];
}
}
} else {
if(typeof val === "function"){
this[i].style[attr] = val();
}else{
this[i].style[attr] = val;
}
}
}
return this;
}
調用方法:
// 獲取樣式屬性值
console.log(mjs(".divBox").css("color"));
// 設置樣式屬性值
// 方法1
mjs(".divBox").css("color","red");
// 方法2
mjs(".divBox").css({
"width":"100px",
"color":"white",
"background-color":"#98bf21",
"font-family":"Arial",
"font-size":"20px",
"padding":"5px"
});
// 方法3
mjs(".divBox").css(
"background-color",function(){
return "#F00"
}
)
find()、first()、last()、eq(index)、parent()
find: function(selector){
return this.init(selector,this[0])
},
first: function(){
return this.init(this[0])
},
last: function(){
return this.init(this[this.length-1])
},
eq: function(index){
return this.init(this[index])
},
parent: function(){
return this.init(this[0].parentNode);
}
我們前面想通過上下文查找內容:
console.log(mjs("span",mjs(".divBox")[0]).html())
我們可以通過find方法這樣寫:
console.log(mjs(".divBox").find("span").html()) console.log(mjs(".divBox span").first().html())
console.log(mjs(".divBox span").last().html())
console.log(mjs(".divBox span").eq(1).html())
console.log(mjs(".divBox span").eq(1).parent().html())
關鍵在于mjs對象和原生dom的區別和相互轉換。
至此我們封裝了一個簡單的類jQuery的工具庫,當然對于一個完整的工具庫,比如jQuery、zepto等,它們功能肯定是更為完善,封裝了更多的方法,在異常處理及性能、可拓展性方法做得更好,由于本文的重點不是為了完成一個完整的庫,在此只是拋磚引玉,只是學習一下常用的思想,有興趣的朋友可以繼續完善這個庫。
mjs github地址:https://github.com/zhaomenghu...
參考文章
MDN javascript 可想造一個屬于你自己的jQuery庫?
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/79868.html
摘要:很多情況下,通常一個人類,即創建了一個具體的對象。對象就是數據,對象本身不包含方法。類是相似對象的描述,稱為類的定義,是該類對象的藍圖或原型。在中,對象通過對類的實體化形成的對象。一類的對象抽取出來。注意中,對象一定是通過類的實例化來的。
showImg(https://segmentfault.com/img/bVTJ3H?w=900&h=385);
馬上就要到七夕了,離年底老媽老爸...
李昌杰
2019-08-16 10:40
評論0
收藏0
摘要:很多情況下,通常一個人類,即創建了一個具體的對象。對象就是數據,對象本身不包含方法。類是相似對象的描述,稱為類的定義,是該類對象的藍圖或原型。在中,對象通過對類的實體化形成的對象。一類的對象抽取出來。注意中,對象一定是通過類的實例化來的。
showImg(https://segmentfault.com/img/bVTJ3H?w=900&h=385);
馬上就要到七夕了,離年底老媽老爸...
Lyux
2019-06-28 13:32
評論0
收藏0
摘要:很多情況下,通常一個人類,即創建了一個具體的對象。對象就是數據,對象本身不包含方法。類是相似對象的描述,稱為類的定義,是該類對象的藍圖或原型。在中,對象通過對類的實體化形成的對象。一類的對象抽取出來。注意中,對象一定是通過類的實例化來的。
showImg(https://segmentfault.com/img/bVTJ3H?w=900&h=385);
馬上就要到七夕了,離年底老媽老爸...
摘要:然而學習布局,你只要學習幾個手機端頁面自適應解決方案布局進階版附源碼示例前端掘金一年前筆者寫了一篇手機端頁面自適應解決方案布局,意外受到很多朋友的關注和喜歡。
十分鐘學會 Fiddler - 后端 - 掘金一.Fiddler介紹 Fiddler是一個http抓包改包工具,fiddle英文中有欺騙、偽造之意,與wireshark相比它更輕量級,上手簡單,因為只能抓http和https數據...
摘要:函數式編程前端掘金引言面向對象編程一直以來都是中的主導范式。函數式編程是一種強調減少對程序外部狀態產生改變的方式。
JavaScript 函數式編程 - 前端 - 掘金引言 面向對象編程一直以來都是JavaScript中的主導范式。JavaScript作為一門多范式編程語言,然而,近幾年,函數式編程越來越多得受到開發者的青睞。函數式編程是一種強調減少對程序外部狀態產生改變的方式。因此,...
cfanr
2019-08-20 16:33
評論0
收藏0
男| 高級講師
閱讀 3152· 2021-11-04 16:09
閱讀 3106· 2021-09-23 11:49
閱讀 3603· 2021-09-09 09:33
閱讀 3604· 2021-08-18 10:22
閱讀 2041· 2019-08-30 15:55
閱讀 3625· 2019-08-30 15:53
閱讀 2653· 2019-08-28 18:08
閱讀 888· 2019-08-26 18:18
<