Javascript高階程式設計 學習筆記

我見江心秋月白發表於2020-11-07

一、資料型別

5種基本資料型別:string, number,Boolean,undefined,null
複雜資料型別:object
1.undefined
宣告未賦值為undefined,未宣告的變數只能進行一種操作:用typeof檢測資料型別為undefined,
但它確實不等於undefined
2.null
null是一個空物件指標,所以typeof(null)=object,undefined派生於null,
所以(null==undefined)=true,
(null===undefined)=false.
bool值為false的情況:空字串,0和nan,null,undefined
3.number
nan的任何操作都為nan,與任何值都不相等,包括它本身。
isNaN(任何型別的引數):判斷是否“不是nan”,會嘗試將引數轉換為陣列,不能轉換的值為true(不是數值)
數值轉換:
Number(引數):
引數:
(1)bool->0,1
(2)null->0
(3)null->0,undefined->nan
所以isNaN(null)=false,isNaN(undefined)=true
(4)string:只含數字(包括浮點)的轉為其所包含的數字,首個數字為0則忽略,包含十六進位制會轉成10進位制,空串轉為0,其餘為NaN
parseInt(引數):
與Number區別:
parseInt(串)=NaN,
parseInt(“123BLUE”)=123,
Number(“123BLUE”)=NaN
parseFloat:類似parseInt
4.string
除了null和undefined之外,其餘型別都有toString()方法,且不需傳引數,

二、變數、作用域與記憶體空間

1.引用變數和值變數
string, number,Boolean,undefined,null為值型別,按值訪問,前後賦值時其實是建立一個值的副本,每個變數都佔一個記憶體空間,不會互相影響。儲存在棧記憶體中。
陣列、物件、函式為引用型別,原型都為object,值儲存在記憶體中,js無法對記憶體直接操作,所以操作物件是值的引用。前後賦值是在改變的值指向同一個地址,所以會互相影響。儲存在堆記憶體中,複製時其實是複製指標,指向同一個物件。
可以用typeof區分值型別,用instanceof區別引用型別:
引用型別instanceof Array/Object/Function
2.引數傳遞
引數只能按值傳遞,引數在函式外部和內部等同於兩個值,不會互相影響,即函式的引數相當於區域性變數。
3.塊級作用域
Js沒有塊級作用域。使用var宣告變數時會被新增到最近的環境中,有兩種環境全域性環境和函式環境。若初始時沒有用var宣告會被新增到全域性變數,但在嚴格模式下會報錯。
4.作用域鏈
只能向上搜尋,區域性環境有權訪問函式作用域的變數,包括父環境及全域性環境。
全域性環境只能訪問全域性變數,無權訪問區域性變數。
5.垃圾回收機制
1)標記清除:垃圾收集器會給記憶體中的所有變數打上標籤,去掉環境變數以及被環境中的變數引用的變數(閉包),剩下的即要清除的。
2)引用計數:釋放引用次數為0的值所佔的記憶體。但迴圈引用即使執行完畢引用依然不為0,所以要手動解除引用。

三、引用型別

Array型別
1.檢測陣列:Array.isArray(陣列名) 陣列名instanceof Array
2.轉換方法
1)陣列.toString(),toLocalString():返回由陣列中每個值以字串形式組成的以逗號間隔的字串
2)陣列.ValueOf():返回字串
3)陣列.join(“分隔符”):返回以給定分隔符為間隔的字串,預設為逗號。
3.棧、佇列方法
Push:向陣列末端新增項,返回陣列長度。
Pop:移除最後一項,返回移除的值。
Shift:移除第一項返回移除的值。
Unshift:在前端新增任意項,返回長度。
4.排序方法:
Reverse():反轉陣列
Sort():呼叫toString方法,比較得到的字串,有時會出錯,可接受一個比較函式作為引數進行排序。

//升序:
Sort(function(val1,val2){
If(val1<val2)
{return -1;}
Else if(val1>val2){
Rreturn 1;}
Else return 0;
})

5.操作方法
1)陣列1.concat(陣列2):返回兩陣列的連結:陣列1+陣列2
2)slice:陣列.Slice[start,end):返回截斷的陣列,左閉右開,若無end引數,則預設截至到末尾,若start為負數,可看作陣列長度加start.
3)splice:
刪除:splice(index,count):從index處開始刪除count個元素,返回刪除的項
插入:splice(index,0,item1,…itemx) 0意為刪除0個元素,返回一個空陣列
替換:splice(index,conut,item1,…itemx) 返回被替換掉的項

6.查詢方法
IndexOf(查詢項,起始位置):返回索引,不存在則返回-1
LastindexOf():從陣列末尾開始查詢。
7.迭代方法:
Some(),filter(),forEach(),map(),every():它們的引數(function(item,index,arr){})
8.歸併方法
Reduce():遍歷所有項
引數:function(prev,cur,index,arr)
Date型別(略)

RegExp型別
/g:全域性模式,匹配所有字串
/i:不區分大小寫
/m:多行模式

Fuction型別

  1. 將函式名是一個包含指標的變數,函式沒有過載,相同的函式名後面宣告的會覆蓋前面。
  2. 函式宣告無變數提升,函式表示式有
  3. Arguments物件:儲存函式引數,函式可以接受任意個引數,可以通過arguments[]來訪問,函式長度是引數的長度即argument.length.
    Arguemnts.callee()指向擁有這個arguments物件的函式,即函式名,但在嚴格模式下會報錯。
  4. this指向的是函式的執行環境,全域性環境this物件引用的是window,當函式是某個物件的呼叫方法時,this就等於那個物件。匿名函式的執行環境具有全域性性,this為window
    Caller屬性:函式名.caller/arguments.callee.caller返回撥用當前函式的函式
  5. call和apply方法:改變函式的作用域 fx.call()/fx.apply()
    apply(var1,var2):var1執行函式的作用域,var2:引數陣列,可以是陣列,也可以是arguments物件,將執行作用域改為var1
    Call(var1,var2):var1同apply,var2:引數直接傳遞,每個都必須列舉出來。
    函式.Call/apply(window):顯示得在全域性作用域中呼叫函式
    函式.Call/apply(this):this繫結作用域,函式在該作用域下執行
Window.color=”red”;
Var o={color:”blue”};
Function saycolor(){
Alert(this.color);
}
Saycolor();//red
Saycolor.apply(this);//red
Saycolor.apply(window);//red
Saycolor.apply(o);//blue
Var osaycolor=saycolor.bind(o);
Osaycolor();//blue

Bind(var1):建立一個函式例項(也是函式),this的值會與var1繫結

基本包裝型別
String

  1. 字元方法:charAt(index),charCodeAt(index):返回index處的字元和字元編碼。與字串[index]訪問相同

  2. 操作方法
    Concat:連線
    建立新字串
    在這裡插入圖片描述

  3. 位置方法
    IndexOf(item,start):從index開始尋找item,start預設為0,返回index.
    LastIndexOf(item,start):從末尾開始尋找
    只能找到一個,不能全找到
    Trim():刪除字首和字尾的所有空格,返回副本,原字串不變
    ToUpperCase,toLowerCase:字串轉換為大小寫。

Math.floor(math.random()*(max-min+1)+min)

四、物件導向的程式設計

建立物件

  1. 工廠模式
Function createPerson(name,age,job){
Var o=new Object();
o.name=name;
o.age=age;
o.job=job;
o.sayname=function(){
alert(this.name)};
return o;
}
Var person1=createPerson(“jane”,18,“student”);
  1. 建構函式模式
Function Person(name,age,job){
This.name=name;
This.age=age;
This.job=job;
This.sayname=function(){
Alert(this.name);}

}
Var person1=new Person(‘”jane”,18,”student”);

New過程:
1) 建立一個物件例項。
2) 將建構函式的作用域賦給新物件例項,this指向新物件例項
3) 執行建構函式,為物件例項新增屬性方法。
4) 返回新物件。
物件例項有一個constructor指標指向建構函式
如person1.constructor=Person
Person1 instanceof Persontrue
Person1 instanceof Object
true//所有的物件均繼承自Object.
建構函式可當成普通函式使用(不加new),this指向window。
可通過call/apply在另一物件作用域中呼叫。
缺點:每個例項的方法都會重複建立(因為函式也是物件),所以方法可放在函式外定義,但封裝性不好。
3. 原型模式

Function Person(){}//建構函式
Person.prototype.name=”jane”;
Person.prototype.age=18;
Person.prototype.job=”student”;
Person.prototype.sayname=function(){
Alert (this.name);};
//字面量建立原型方法
Function Person(){}//建構函式
Person.prototype={
Constructor:Person;//若不說明,則constructor指向Object
Name:”jane”;
Age:18;
Job:”student”;
Sayname:function(){
Alert (this.name);}
};

Var person1=new Person();
Person1.sayname();//jane

在這裡插入圖片描述

建構函式有一個prototype指標指向函式原型Person.prototype,函式原型也有個Person.prototype.constructor指標指向建構函式,所有的方法屬性在物件原型中新增。當呼叫建構函式建立一個例項後,物件例項和建構函式無直接關係,它也有一個prototype指標指向原型物件,原型物件中的所有方法屬性都能被所有物件例項共享。
讀取物件例項的某個屬性後,會從例項本身開始搜尋該屬性,如不存在再繼續搜尋指標指向的原型。如果再例項中新增一個同名物件,該屬性會先遮蔽原型中屬性,但原型的屬性仍然存在。
例項.hasOwnPrototype(“屬性”):可檢視該屬性是否屬於例項本身,不是來自於原型,返回bool值
“屬性”in例項:返回bool值,無論該屬性存在於例項還是原型中。
Object.key(物件):返回包含所有可列舉屬性的字串陣列。如果通過例項呼叫,只能返回例項自身的屬性。
原型具有動態性:即使先建立例項再修改原型,仍然能準確訪問,但是重寫整個原型物件則會產生錯誤,因為將原型修改為另一物件等同於切斷了例項和原型的聯絡。
缺點:雖然同名屬性會遮蔽原型屬性,但是每個例項對原型中引用型別的屬性修改會互相影響。
解決:組合使用建構函式模式和原型模式
建構函式用於定義例項屬性和引用型別屬性,原型用於定義方法和共享屬性。

Function Person(name,age,friend){
This.name=name;
This.age=age;
This.friend=[“kk”,”mc”];
}
Person.prototype={
Constructor:Person;
Sayname:function(){
Alert(this.name);}
};

4.原型鏈
1)

Function Super(){
This.property=true;
}
Super.prototype.getval=function(){
Return this.property;
}	
Function Sub(){
This.subproperty=false;
}
Sub.prototype=new ppSuper();//sub原型物件是super的例項
Sub.prototype.getsubval function(){
Return this.subproperty;
}
Var o=new Sub();

因為sub繼承super,所以未使用sub預設提供的原型,而是換上一個新原型,這個新原型是super的例項,作為例項它和普通例項一樣有一個prototype指標指向建構函式的原型。Sub繼承super的所有方法屬性,super的例項屬性被新增到sub的原型中。

在這裡插入圖片描述

所有函式的預設原型都是Object的例項,Object.prototype是原型鏈的頂端。

在這裡插入圖片描述

存在的問題:1)父類super的所有原型例項都會有自己的引用屬性,但是子類的例項共享一個引用屬性,修改會互相影響。(從繼承父類(原型屬性的繼承)獨自擁有,從子類繼承(例項(子類也是例項)屬性的繼承)共享)
2)不能向父類的建構函式傳遞引數

2)解決方法:
借用建構函式:在子類建構函式內部呼叫父類建構函式
Function Sub(){
Super.call(this);}
Var o=new Sub();//結合new過程第二步:this指向o,執行建構函式,同過call(this)在物件o上執行Super建構函式,sub的例項不經過sub的建構函式直接呼叫父類super的建構函式,從而擁有自己的引用屬性副本。傳遞引數也可以通過這種方法實現,如:super.call(this,”jane”)

3)組合繼承
使用原型鏈實現原型屬性和方法的繼承,把例項屬性放在建構函式裡然後通過借用建構函式實現對例項屬性的繼承。
4)寄生組合式繼承(存疑)
通過借用建構函式繼承屬性,原型鏈的混合模式繼承方法。不必兩次呼叫父類的建構函式,省去了建立子類原型的呼叫。

五、函式表示式

1.遞迴:用原函式名實現遞迴,當函式名被更改時會出錯(因為函式是引用型別),可以用arguments.callee代替函式名,但在嚴格模式下會出錯,可以將整個遞迴函式賦值給一個變數。
2.函式呼叫過程
當函式fx被呼叫時,會建立一個執行環境和相應的作用域鏈。作用域鏈首端是該函式的活動變數,包括arguments和其他命名引數,後一位是外部函式的活動物件等等,直至作為全域性執行環境的全域性變數物件。全域性變數始終存在,函式內的活動物件在函式執行完就被銷燬,只在函式執行過程中存在。作用域鏈實際是一個指向變數物件的指標列表。
但閉包情況又有所不同
3.閉包
閉包指在函式內部建立另一個函式,它有權訪問另一函式作用域內的變數。閉包的作用域處理其自身活動變數和全域性變數之外,還包括其外部函式的活動變數,所以它就有權訪問其外部函式的變數。當外部函式執行完後,它的活動物件也不會被銷燬。因為閉包正在引用它的活動物件,也就是其執行環境作用域鏈已被銷燬,但活動變數還留在記憶體中,直至閉包銷燬才會被銷燬。
可將閉包設定為null以解除引用,釋放記憶體。過度使用閉包會佔用更多記憶體,所以避免使用閉包。
在這裡插入圖片描述

4.閉包只能獲取閉包函式變數的最後一個值
如:

Function fx{
Var result=new Array();
for(var i=0;i<10;i++){
 result[i]=function(){
return i;
};
 return result;
}//result全為10
改進:
Function fx{
Var result=new Array();
for(var i=0;i<10;i++){
 result[i]=function(num){
function(){
return num;
}(i);
}
return result;
}

引數按值傳遞,將i值賦給num.
4.閉包中的this和arguments
每個函式在呼叫時都會自動獲取兩個變數:this和arguments.所以閉包搜尋this和arguments時只會搜尋到自身的活動變數,從中或這兩個變數,不會訪問外部函式中的這兩個變數()因此若閉包為匿名函式則this為window.
可以將外部函式的this儲存在一個閉包能訪問到的變數裡。
如下:

Var name=”my window”;
var o={
name:”jane”;
getname:function(){
return function(){
return this.name;};}
};
alert(o.getname);//my window
修改:
Var name=”my window”;
var o={
name:”jane”;
getname:function(){
var that=this;
return function(){
return that.name;};}
};
alert(o.getname);//my window

5.模仿塊級作用域
js沒有塊級作用域,只有全域性和函式作用域。
可以用匿名函式模仿塊級作用域。
(function(){})()
等同於var var1=function(){};var1();(function(){})加括號是為了不被js當成函式宣告。
把程式碼放進這個匿名函式裡就可以用函式作用域模仿塊級作用域。
初始化未經宣告的變數,該變數為全域性變數,但在嚴格模式下會出錯。

六、BOM

1.視窗位置
獲取瀏覽器視窗左邊和上邊的位置:左上角是圓點(0,0)
var leftpos=(typeof window.screenLeft==”number”)?window.sreenLeft:window.screenX;
var toppos=(typeof window.screenTop==”number”)?window.sreenTop:window.screenY;
移動視窗:window.moveTo(x,y)移動到點(x,y)window.moveBy(x,y):向左(負)/右(正)移動x,向上(負)/下(正)移動y.
但這兩種方法在opera即ie7及更高版本被禁用。
2.瀏覽器視窗大小:outerWidth和outerHeight
檢視區大小(減去邊框):innnerWidth和innerHeight
在chorme中都是檢視區大小
3.window.open(url,新視窗/”blank”)
window.location:獲取url,location:hash,post,pathname,href,search,protocol
navigator.userAgent:使用者代理檢測
history.go(n):前進n頁,history.go(-n):後退n頁,history.go(url):跳轉到最近的url
前進:history.foward(),後退:history.back()
瀏覽器核心:firefox:gecko,safari\chorme\ios\android:webkit

七.DOM(略)

1.js中的所有節點型別都繼承自Node型別。方法:appendChild(),removeChild(),
replaceChild(),insertBefore()
2.Document型別表示整個文件,是根節點。document物件是Document類的一個例項
獲取第一個節點:document.documentElement、childNodes[0]、firstChild
獲取其他:document.body、document.doctype、document.URL, document.domain,
.anchors(獲取所有帶name的a標籤),.form,.images,.links(獲取所有帶link的a標籤)
getElementById(),getElementsByTagName(),getElementByName()
文件寫入:document.write()/writeln()
3.Element型別
獲取設定移除特性:getAttribute(),setAttribute(),removeAttribute(),
建立屬性:
var div=document.createElement(“div”),document.body.appendChild(div)
querySlector(),querySlectorAll()

八、事件

1.事件流:從頁面中接收物件的順序
事件冒泡:從具體元素開始接收,逐級向上傳遞直到document物件(IE5.5及更早會跳過html元素,IE9,firefox,chorme和safari會冒泡到window物件)
事件捕獲:從不具體節點到具體節點(IE不支援,不常用)
DOM事件流:事件捕獲階段,具體節點接收階段,事件冒泡階段
2.事件處理程式:var btn=document.getElementById(“btn”);
在這裡插入圖片描述

跨瀏覽器的事件處理程式

var EventUtil={
addHander:function(element(元素),type(事件),hander(處理程式)){
if(element.addEventListener){
element. addEventListener(type,hander,false)
}
else if(element.attachEvent){
element.attachEvent(“on”+type,hander);
}
else{ element[“on”+click]=hander;}
},
removeHander:function(){}
};
EventUtil.addHander(btn,”click”,hander)

3.事件物件event
DOM事件物件(DOM0和DOM1)
event傳入事件處理程式中
btn.οnclick=function(event){}
btn.addEventListener(“click”,function(event){},false)
event的屬性:
event.target:事件的目標
event.currentTarget:指向繫結事件處理程式的元素,值與this相等。
event.type:事件的型別
event的方法:
event.preventDefault():阻止預設行為
event.stopPropagation:阻止冒泡

九、AJAX與comet

ajax不用重新整理頁面就能從伺服器獲取資料,comet伺服器向頁面推送資料(伺服器推送)
1.XMLHttpRequest物件
var xhr=new XMLHttpRequest();
XMLHttpRequest物件的方法:
建立請求:XMLHttpRequest.open(“get”/”post”,”url“,false/true)bool值表示是否非同步。
傳送請求:XMLHttpRequest.send(var):var為要傳送的資料,若不需要傳送資料則var=null.
2.同步
等伺服器響應請求後才會繼續執行,收到響應後會自動填充xhr物件的屬性:
responseText:伺服器返回的文字
responseXML:響應資料的XML DOM資料
status:響應的http狀態
statusText:http狀態的說明
status:200表示成功,304表示請求資源沒有被更改,可以使用瀏覽器中快取的版本。
var xhr=new XMLHttpRequest();
xhr.open(“get”,”example.txt”,false);
xhr.send(null);
if((xhr.status>=200&&xhr.status<300)||xhr.status==304){
alert(xhr.responseText);
}
else{alert(“wrong”+xhr.status);}

3.非同步
不必等待響應
readyState屬性表示請求/響應過程的活動狀態:
0:未初始化,尚未呼叫open()
1:啟動。已open()未send()
2:傳送。已send()未接受響應
3:接收。已接受到部分響應
4:完成。接收到全部響應。
readyState變化會觸發readystatechange事件,在open之前為xhr繫結此事件

xhr.onreadystatechange=function(){
if(xhr.readystate==4){
if((xhr.status>=200&&xhr.status<300)||xhr.status==304){
alert(xhr.responseText);
}
else{alert(“wrong”+xhr.status);}
}
};
xhr.open(“get”,”example.txt”,false);
xhr.send(null);

4.get,post請求
post作為請求主題提交,可以包含很多資料且格式不限
get查詢字串每個引數:名=值
5.跨域資源共享(cors)
xhr實現ajax通訊,有跨域限制,xhr物件是能訪問同埠、同協議、同域名的資源。
cors可以實現跨域:使用自定義的http頭部讓瀏覽器與伺服器通訊。
IE實現cors:引入XDR物件
var xdr=new XdomianRequest()與xhr類似,不同的是它只有兩個引數(get/post,url)只支援非同步。
其他瀏覽器實現cors:使用標準xhr物件並在open中傳入絕對url即可.
6.JSONP跨域
格式包含在回撥函式中的json:callback{{“name”:”jane”}};
包含兩部分:回撥函式和資料
回撥函式在url中指定,資料是傳入回撥函式的json資料
如:https://free.net/json/?callback=handleresponse
因為<script>不受跨域影響,所以可以為script.src指定一個跨域url
7.comet
兩種實現方法:1)長輪詢:頁面向伺服器傳送請求,伺服器保持連線開啟,直到有資料可傳送,傳送完後瀏覽器關閉連線,隨即又傳送新請求。
2)http流:瀏覽器向伺服器傳送一個請求,伺服器保持連線開啟,週期性地向瀏覽器傳送資料。
通過監聽readystatechange事件,readystate為3時,分割reponseText以獲取最新資料,通過progress回撥函式處理傳入的新資料,當readyState為4時,執行finished回撥函式傳入響應的全部內容。
7.Web Socket API
實現全雙工,雙向通訊
使用自定義的模式ws://(而不是http), wss://(而不是https)
var socket=new Web Socket(“ws://www.example.com/”)
傳入絕對url,支援跨域
readyState:
0:正在建立連線
1:已經建立連線 socket.open()
2:正在關閉連線
3:已經關閉連線 socket.close()
傳送資料:socket.send()

8.CRSF(跨站點請求偽造)
未授權系統偽造自己使之有權訪問某個資源,竊取資料或銷燬資料
預防:每次請求都要附帶經過相應演算法計算得到的驗證碼

十、高階技巧

1.函式繫結
bind可以將函式繫結到指定的環境,bind接收一個函式和一個環境,返回一個在給定環境中呼叫給定函式的函式,並原封不動的傳遞所有引數

function bind(fn,context){
return fn.call(context,arguments);
}

2.函式柯里化
將返回的函式引數進行排序
第一個引數是要可裡化的函式,其他引數是要傳入的值。用slice(arguments,1)取得外部函式除第一個引數外(第一個引數是外部函式名)的所有引數,用slice(arguments)獲取內部函式所有引數,用concat將引數組合,最後將結果傳給該函式。

function curry(fn){
var args=Array.prototype.slice.call(arguments,1);
return function(){
var innerargs= Array.prototype.slice.call(arguments);
var fianlargs=args.concat(innerargs);
return fn.call(null,finalargs);
};
}
function add(num1,num2){
return num1+num2;}
var cur=curry(add,1);
alert(cur(3));//4

與繫結函式結合:在任意環境以任意引數執行任意函式

function bind(fn,context){
var args=Array.prototype.slice.call(arguments,2);//除了fn和context
return function(){
var innerargs= Array.prototype.slice.call(arguments);
var fianlargs=args.concat(innerargs);
return fn.call(context,finalargs);
}

3.定時器
定時器的事件間隔是指何時將定時器的程式碼放入等待佇列中。
函式節流:某些程式碼不可以沒有間斷的執行
function throttle(method,context){//引數為要執行的啊含糊和作用域
clearTimeout(method.tId);//先清楚之前的定時器
method.tId=setTimeout(function(){//新建一個定時器
method.call(context);},100);//定時器在當前環境執行
定時器ID儲存於tId

十一、客戶端儲存

1.離線檢測:nevigator.onLine屬性
方法:nevigator.online:離線變線上時觸發
nevigator.offline:線上變離線時觸發
2.cookie,大小在4095b以內
3.web儲存機制:sessionStorage和globalStorage(h5中用localStorage代替)
1)sessionStorage是storage的例項
用來儲存某個會話的資料,暫時的,瀏覽器關閉即消失
使用方法儲存資料:sessionStorage.setItem(“name”,”jane”)
使用屬性儲存資料:sessionStorage.name=“jane”
使用方法獲取資料:sessionStorage.getItem(“name”,”jane”)
使用屬性獲取資料:var name=sessionStorage.name
2)globalStorage,跨越會話儲存資料,所以要指定在哪個域可以訪問資料。
globalStorage[“域名”]:才是是storage的例項
使用方法儲存資料:globalStorage[“域名”].setItem(“name”,”jane”)
使用屬性儲存資料:globalStorage[“域名”].name=“jane”
使用方法獲取資料:globalStorage[“域名”].getItem(“name”,”jane”)
使用屬性獲取資料:var name= globalStorage[“域名”].name
3)localStorage:訪問時,頁面必須要同協議,同域名,同埠
使用方法儲存資料:localStorage.setItem(“name”,”jane”)
使用屬性儲存資料:localStorage.name=“jane”
使用方法獲取資料:localStorage.getItem(“name”,”jane”)
使用屬性獲取資料:var name=localStorage.name

相關文章