ESLint規則中的JavaScript知識

lijiaxunOuO發表於2018-07-24

網上關於ESLint的介紹,安裝,配置和使用等文章已經有很多了,寫的都很全面,還不清楚的同學可以先去簡單瞭解一下,本文就不作介紹了。

本文的主要內容是通過ESLint的規則(Rules),從中學到 JavaScript 的基礎知識。同時,提前瞭解這些規則的含義與理由,有助於在以後的開發中提前規避風險,提高程式碼質量。

以下是從官網的 Rules 列表中摘取的部分規則,歡迎大家補充,共同學習。

Possible Errors

getter-return:強制在 getter 屬性中出現一個 return 語句。每個 getter 都期望有返回值。

no-compare-neg-zero:禁止與 -0 進行比較。像 x === -0 的程式碼對於 +0-0 都有效。你可以使用 Object.is(x, -0)。例:

// incorrect
if (x === -0) {
    // doSomething()...
}

// correct
if (x === 0) {
    // doSomething()...
}
if (Object.is(x, -0)) {
    // doSomething()...
}
複製程式碼

no-cond-assign: 禁止在條件語句中出現賦值操作符。在條件語句中,很容易將一個比較運算子(像 ==)錯寫成賦值運算子(如 =)。在條件語句中,使用賦值操作符是有效的。然而,很難判斷某個特定的賦值是否是有意為之。該規則有兩個可選值:

  • except-parens:預設值,允許條件語句中出現賦值操作符,前提是它們被圓括號括起來。
  • always: 禁止條件語句中出現賦值語句。

no-console:禁止呼叫 console 物件的方法。console 這樣的訊息被認為是用於除錯的,生產環境中不應該有關於console 的語句。同時,被console.log的變數是不會被垃圾回收的,一旦多起來會導致記憶體洩漏。該規則有可配置選項allow,它的值是個字串陣列,包含允許使用的console物件的方法。例如: allow: ["warn", "error"]允許使用console物件上的warnerror方法。

no-constant-condition:禁止在條件中使用常量表示式。

no-dupe-args: 禁止在 function 定義中出現重複的引數。

no-dupe-keys: 禁止在物件字面量中出現重複的鍵。

no-duplicate-case: 禁止在 switch 語句中的 case 子句中出現重複的測試表示式。

no-empty: 禁止空語句塊出現,該規則會忽略包含一個註釋的語句塊。例如,在 try 語句中,一個空的 catchfinally 語句塊意味著程式應該繼續執行,無論是否出現錯誤。該規則有個可配置的選項 allowEmptyCatch: true 允許出現空的 catch 子句。

no-ex-assign:禁止對 catch 子句中的異常重新賦值。如果意外地(或故意地)給異常引數賦值,是不可能引用那個位置的錯誤的。由於沒有 arguments 物件提供額外的方式訪問這個異常,對它進行賦值絕對是毀滅性的。

no-func-assign:禁止對 function 宣告重新賦值。例如:

// incorrect
function foo() {}
foo = bar;

function foo() {
 foo = bar;
}
複製程式碼

JavaScript 函式有兩種形式:函式宣告 function foo() { ... } 或者函式表示式 var foo = function() { ... } 。雖然 JavaScript 直譯器可以容忍對函式宣告進行覆蓋或重新賦值,但通常這是個錯誤,應該避免。

no-inner-declarations:禁止在巢狀的塊中出現變數宣告或 function 宣告。可選值(string):functions(預設)both(禁止 function 和 var 宣告出現在巢狀的語句塊中)。這隻適用於函式宣告,命名的或匿名的函式表示式是可以出現在任何允許的地方。在 ES6 之前的 JavaScript 中,函式宣告只能在程式或另一個函式體的頂層。由於變數宣告提升,把宣告放在程式或函式體的頂部會使程式碼更清晰,在任何地方隨意宣告變數的做法通常是不可取的。因為在ES6中的 letconst 不會被提升,因此它們不受此規則影響。

no-irregular-whitespace:禁止在字串和註釋之外不規則的空白。無效的或不規則的空白會導致 ECMAScript 5 解析問題,也會使程式碼難以除錯(類似於混合 tab 和空格的情況)。 引起的問題以及禁止出現的不正常字元

no-obj-calls:禁止把全域性物件作為函式呼叫,如 MathJSONReflect等。

no-sparse-arrays:禁用稀疏陣列,也就是逗號之前沒有任何元素的陣列。該規則不適用於緊隨最後一個元素的拖尾逗號的情況。例如:

// incorrect
var arr = [,,];
var colors = [ "red",, "blue" ];

// correct
var arr = [];
var arr = new Array(23);
var colors = [ "red", "blue", ];
複製程式碼

no-unexpected-multiline:禁止使用令人困惑的多行表示式。在 JavaScript 中,分號通常是可選的,因為會自動插入分號(ASI)。換行不結束語句,書寫錯誤遺漏了分號,這些異常會導致兩個不相干的連續的行被解釋為一個表示式。特別是對於一個沒有分號的程式碼風格,讀者可能會忽略這些錯誤。儘管語法上是正確的,程式碼執行時可能會丟擲異常。

no-unreachable:禁止在 returnthrowcontinuebreak 語句後出現不可達程式碼。因為這些語句無條件地退出程式碼塊,其之後的任何語句都不會被執行。

use-isnan:禁止與 NaN 的比較,要求呼叫 isNaN()檢查 NaN。在 JavaScript 中,NaNNumber 型別的一個特殊值。它被用來表示非數值(Not A Number),這裡的數值是指在 IEEE 浮點數算術標準中定義的雙精度64位格式的值。

console.log(typeof NaN); // "number"
複製程式碼

因為在 JavaScriptNaN 獨特之處在於它不等於任何值,包括它本身,與 NaN 進行比較的結果也令人困惑:

console.log(NaN !== NaN); // true
console.log(NaN === NaN); // false
複製程式碼

因此,使用 Number.isNaN() 或 全域性的 isNaN() 函式來測試一個值是否是 NaN

valid-typeof:強制 typeof 表示式與有效的字串進行比較。對於絕大多數用例而言,typeof 操作符的結果是以下字串字面量中的一個:"undefined""object""boolean""number""string""function""symbol"。把 typeof 操作符的結果與其它字串進行比較,通常是個書寫錯誤。例:

// incorrect
typeof foo === "strnig"
typeof foo == "undefimed"
typeof bar != "nunber"
typeof bar !== "function"

// correct
typeof foo === "string"
typeof bar == "undefined"
typeof foo === baz
typeof bar === typeof qux
複製程式碼

Best Practices

accessor-pairs:強制getter/setter成對出現在物件中。該規則強制一種編碼風格:對於每個屬性,如果定義了setter,也必須定義相應的 getter。沒有 getter ,你不能讀取這個屬性,該屬性也就不會被用到。

array-callback-return:強制陣列某些方法的回撥函式中有 return 語句。 Array 有一些方法用來過濾、對映和摺疊。如果你忘記了在它們的回撥函式中寫 return 語句,這種情況可能是個錯誤。需要 return 語句的方法有:

  • Array.from
  • Array.prototype.every
  • Array.prototype.filter
  • Array.prototype.find
  • Array.prototype.findIndex
  • Array.prototype.map
  • Array.prototype.reduce
  • Array.prototype.reduceRight
  • Array.prototype.some
  • Array.prototype.sort

default-case:要求 wwitch 語句中有 default 分支,即使預設分支中沒有任何程式碼。開發人員可能忘記定義預設分支而導致程式發生錯誤,所以明確規定定義預設分支是很好的選擇。或者也可以在最後一個 case 分支下,使用 // no default 來表明此處不需要 default 分支。

eqeqeq:要求使用 ===!== 代替 ==!= 操作符。原因是 ==!= 在比較時會作強制轉型,有時會產生副作用甚至異常。 有兩個可選值:

  • always : 強制在任何情況下都使用 ===!==
  • smart: 除了以下這些情況外,強制使用 ===!==
    • 比較兩個字面量的值
    • 比較 typeof 的值
    • null 進行比較

no-alert:禁用 alertconfirmpromptJavaScriptalertconfirmprompt 被廣泛認為是突兀的 UI 元素,應該被一個更合適的自定義的 UI 介面代替。此外, alert 經常被用於除錯程式碼,部署到生產環境之前應該刪除。因此,當遇到 alertpromptconfirm 時,該規則將發出警告。

no-caller:禁用 arguments.callerarguments.callee。它們的使用使一些程式碼優化變得不可能。在 JavaScript 的新版本中它們已被棄用,同時在 ECMAScript 5 的嚴格模式下,它們也是被禁用的。

no-case-declarations:禁止詞法宣告 (letconstfunctionclass) 出現在 casedefault 子句中。詞法宣告在整個 switch 語句塊中是可見的,但是它只有在執行到它定義的 case 語句時,才會進行初始化操作。 為了保證詞法宣告語句只在當前 case 語句中有效,將你子句包裹在塊中。例:

// incorrect
switch (foo) {
    case 1:
        let x = 1;
        break;
    case 2:
        const y = 2;
        break;
    case 3:
        function f() {}
        break;
    default:
        class C {}
}

// correct
switch (foo) {
    // 下面的 case 子句使用括號包裹在了塊中
    case 1: {
        let x = 1;
        break;
    }
    case 2: {
        const y = 2;
        break;
    }
    case 3: {
        function f() {}
        break;
    }
    case 4:
        // 因為函式作用域提升,使用 var 宣告而不使用括號包裹是合法的。
        var z = 4;
        break;
    default: {
        class C {}
    }
}
複製程式碼

no-else-return:禁止 if 語句中 return 語句之後有 else 塊。如果 if 塊中包含了一個 return 語句,else 塊就成了多餘的了。可以將其內容移至塊外。例:

// incorrect
function foo() {
    if (x) {
        return y;
    } else {
        return z;
    }
}

// correct
function foo() {
    if (x) {
        return y;
    }

    return z;
}
複製程式碼

no-empty-function:禁止出現空函式。空函式能降低程式碼的可讀性,如果一個函式包含了一條註釋,它將不會被認為有問題。該規則有一個選項,配置所允許的空函式列表,預設為空陣列。

no-empty-pattern:禁止使用空解構模式。當使用解構賦值時,可能建立了一個不起作用的模式。把空的花括號放在嵌入的物件的解構模式右邊時,就會產生這種情況。例:

// incorrect
var {} = foo;
var [] = foo;
var {a: {}} = foo;
var {a: []} = foo;
function foo({}) {}
function foo([]) {}
function foo({a: {}}) {}
function foo({a: []}) {}

// correct
var {a = {}} = foo;
var {a = []} = foo;
function foo({a = {}}) {}
function foo({a = []}) {}
複製程式碼

no-eq-null:禁止使用 ==!= 操作符與 null 進行比較。當你進行比較時可能得意想不到的的結果,因為 nullnullundefined 的比較結果都為 true

no-eval:禁用 eval()JavaScript 中的 eval() 函式是有潛在危險的,而且經常被誤用。eval() 在大多數情況下可以被更好的解決問題的方法代替。

no-global-assign:禁止對原生物件或只讀的全域性物件進行賦值。JavaScript 環境包含很多內建的全域性變數,比如瀏覽器環境的 windowNode.js 中的 process。在幾乎所有情況下,你都不應該給全域性變數賦值,因為這樣做可能會到導致無法訪問到重要的功能。

no-implicit-globals:禁止在全域性範圍下使用 var 和命名的 function 宣告。因為這樣,會作為 window 物件的一個屬性或方法存在。全域性變數應該顯式地賦值給 windowself。否則,區域性變數應該包裹在 IIFE 中。該規則不適用於 ESCommonJS 的模組,因為它們有自己的模組作用域。

no-invalid-this:禁止 this 關鍵字在類或類物件之外出現。該規則 在嚴格模式下生效,在嚴格模式下,類或者類物件之外的 this 關鍵字可能是 undefined 並且引發 TypeError

no-iterator:禁用 __iterator__ 屬性。這個屬性現在廢棄了,所以不應再使用它。現在,應該使用 ES6 迭代器和生成器。

no-lone-blocks:禁用不必要的巢狀塊。在 ES6 之前,由花括號分隔開的獨立程式碼塊不會建立新的作用域,也就不起什麼作用,程式碼塊是多餘的。例:

{ // 該括號對 foo 不起任何作用
    var foo = bar();
}
複製程式碼

ES6 中,如果出現一個塊級繫結 (letconst),類宣告或函式宣告(在嚴格模式下),程式碼塊就會建立一個新的作用域。在這些情況下,程式碼塊不會被認為是多餘的。

no-multi-spaces:禁止在邏輯表示式、條件表示式、宣告、陣列元素、物件屬性、序列和函式引數周圍使用多個空格。

no-new:禁止使用 new 關鍵字呼叫建構函式但卻不將結果賦值給一個變數。比如 new Thing() 建立的物件會被銷燬因為它的引用沒有被儲存在任何地方。

no-new-func:禁止對 Function 物件使用 new 操作符。JavaScript 中可以使用 Function 建構函式建立一個函式,如 var x = new Function("a", "b", "return a + b");,把一個字串傳給 Function 建構函式,你需要引擎解析該字串,這一點同呼叫 eval 函式一樣,應該避免這樣使用。

no-new-wrappers:禁止對 StringNumberBoolean 使用 new 操作符。在 JavaScript 中有3種原始型別包裝物件:字串,數字和布林值。它們所代表的構造器分別為 StringNumberBoolean。 下面的例子使用 new 操作符後使用 typeof 將返回 "object",而不是 "string", "number"“boolean”,這意味著可能與預期不符。

var stringObject = new String("Hello world");
var numberObject = new Number(33);
var booleanObject = new Boolean(false);
複製程式碼

而且,每個物件的判斷都是真,這意味著每個 Boolean 的例項都會返回 true,即使它們實際的值是 false。所以,應該避免使用 new 來使用原始包裝型別。一般情況下,下面這樣使用即可。

var text = String(someValue);
var num = Number(someValue);
var bol = Boolean(someValue);
複製程式碼

no-param-reassign:禁止對 function 的引數進行重新賦值。比如:function f(arg) { arg = 1; }function f(obj) { obj.num = 1; }。對函式引數中的變數進行賦值可能會誤導讀者,導致混亂,也會改變 arguments 物件。如果引數是引用型別,比如物件,修改物件的屬性會影響到傳入函式的那個原始物件。如果需要修改可以複製一份資料再改。

no-proto:禁用 __proto__ 屬性。__proto__ 屬性在 ECMAScript 3.1 中已經被棄用,並且不應該在程式碼中使用。應該使用 getPrototypeOf 方法替代 __proto__getPrototypeOf是獲取原型的首選方法。

no-redeclare:禁止多次宣告同一變數。這會使變數實際宣告和定義的位置混亂不堪。

no-return-assign:禁止在 return 語句中使用賦值語句。因為很難斷定 return 語句的意圖。可能是賦值,但賦值的意圖也可能不明確,也可能是比較。

no-self-assign:禁止自身賦值。自身賦值不起任何作用。

no-self-compare:禁止自身比較。幾乎沒有場景需要你比較變數本身。

no-unmodified-loop-condition:禁用一成不變的迴圈條件。迴圈條件中的變數在迴圈中是要經常改變的。如果不是這樣,那麼可能是個錯誤。

no-useless-call:禁止不必要的 .call().apply()。例如下面的程式碼與 foo(1, 2, 3)效果相同:

foo.call(undefined, 1, 2, 3);
foo.apply(undefined, [1, 2, 3]);
foo.call(null, 1, 2, 3);
foo.apply(null, [1, 2, 3]);
複製程式碼

函式的呼叫可以寫成 Function.prototype.call()Function.prototype.apply(),但是 Function.prototype.call()Function.prototype.apply() 比正常的函式呼叫效率低。

no-useless-concat:禁止不必要的字串字面量或模板字面量的連線。把兩個字元拼接在一起是沒有必要的,比如: var foo = "a" + "b"; 直接寫作 var foo = "ab"; 即可。

no-useless-escape:禁用不必要的轉義字元。對字串、模板字面量和正規表示式中的常規字元進行轉義,不會對結果產生任何影響,但它是多餘的。

// 不必要使用轉義符
"\'";
'\"';
"\#";
"\e";
`\"`;
`\"${foo}\"`;
`\#{foo}`;
/\!/;
/\@/;

// 需要使用轉義符
"\"";
'\'';
"\x12";
"\u00a9";
"\371";
"xs\u2111";
`\``;
`\${${foo}}`;
`$\{${foo}}`;
/\\/g;
/\t/g;
/\w\$\*\^\./;
複製程式碼

prefer-promise-reject-errors:要求使用 Error 物件作為 Promise 拒絕的原因。Error 物件會自動儲存堆疊跟蹤,在除錯時,通過它可以用來確定錯誤是從哪裡來的。如果 Promise 使用了非 Error 的值作為拒絕原因,那麼就很難確定 reject 在哪裡產生。

require-await:禁止使用不帶 await 表示式的 async 函式。async 函式不包含 await 函式可能不是重構想要的結果。

vars-on-top:要求所有的 var 宣告出現在它們所在的作用域頂部。預設的,JavaScript 的解析器會隱式的將變數的宣告移到它們所在作用域的頂部("變數提升")。這個規則迫使程式設計師通過手動移動變數宣告到其作用域的頂部來實現這個行為,有助於提高可維護性。

wrap-iife:要求 IIFE 使用括號括起來。你可以立即呼叫函式表示式,而不是函式宣告。建立一個立即執行函式 (IIFE) 的一個通用技術是用括號包裹一個函式宣告。括號內的函式被解析為一個表示式,而不是一個宣告。

Variable Declarations

no-delete-var:禁止刪除變數。delete 的目的是刪除物件的屬性。使用 delete 操作刪除一個變數可能會導致意外情況發生。

no-label-var:不允許標籤與變數同名。

no-shadow:禁止變數宣告與外層作用域的變數同名。例:

var a = 3;
function b() {
    var a = 10;
}
複製程式碼

b() 作用域中的 a 覆蓋了全域性環境中的 a。這會混淆讀者並且在 b 中不能獲取全域性變數。

no-shadow-restricted-names:禁止重定義遮蔽關鍵字。全域性物件的屬性值 (NaNInfinityundefined)和嚴格模式下被限定的識別符號 evalarguments 也被認為是關鍵字。重定義關鍵字會產生意想不到的後果且易迷惑其他讀者。

no-undef-init:禁止將變數初始化為 undefined。在 JavaScript 中,宣告一個變數但未初始化,變數會自動獲得 undefined 作為初始值,因此,初始化變數值為 undefined 是多餘的。

no-unused-vars:禁止出現未使用過的變數。已宣告的變數在程式碼裡未被使用過,就像是由於不完整的重構而導致的遺漏錯誤。這樣的變數增加了程式碼量,並且混淆讀者。

no-use-before-define:禁止在變數定義之前使用它們。在 ES6 標準之前的 JavaScript 中,某個作用域中變數和函式的宣告會被提前到作用域頂部("變數提升"),所以可能存在這種情況:此變數在宣告前被使用。這會擾亂讀者。在 ES6 中,塊級繫結 (letconst) 引入"暫時性死區",當企圖使用未宣告的變數會丟擲 ReferenceError

Node.js and CommonJS

global-require:要求 require() 出現在頂層模組作用域中。在 Node.js 中,使用 require() 函式引入依賴的模組,它在模組頂層被呼叫,這樣更容易識別依賴關係。當它們在深層次巢狀在函式和其它語句時,就很難識別依賴。因為 require() 是同步載入的,在其它地方使用時,會導致效能問題。此外,ES6 模組要求 importexport 語句只能放在模組頂部。

handle-callback-err:要求回撥函式中有容錯處理。在 Node.js 中,最普遍的處理非同步行為是回撥模式。這個模式期望一個 Error 物件或 null 作為回撥的第一個引數。如果忘記處理這些錯誤會導致你的應用程式出現一些非常奇怪的行為。

no-buffer-constructor:禁用 Buffer() 建構函式。在 Node.js 中,Buffer 建構函式的行為取決於其引數的型別。將使用者輸入的引數傳遞給 Buffer(),而不驗證其型別,會導致安全漏洞,比如遠端記憶體洩漏和拒絕服務。因此,Buffer 建構函式已經被棄用,不應該再使用。使用 Buffer.fromBuffer.allocBuffer.allocUnsafe 生成器方法代替。

no-new-require:禁止呼叫 require 時使用 new 操作符。require 方法被用來引入不同檔案中模組。某些模組可能返回一個建構函式,會出現 var app = new (require(moduleName)); 的情況,這樣可能會引起潛在的混亂,應該避免這樣的情況,分成多行寫會使你的程式碼更清晰。

Stylistic Issues

關於空格,換行,宣告,標點符號等風格規則,非常主觀,依據個人或團隊編碼風格自定義,這裡不作介紹。

ECMAScript 6

這些規則只與 ES6 有關,即通常所說的 ES2015

arrow-body-style:要求箭頭函式體使用大括號。為了規避箭頭函式語法可能帶來的錯誤,當函式體只有一行的時候,若不加大括號,會預設把這行程式碼的返回結果給隱式 return。當函式體有多行的時候,必須使用大括號,並且需要自己寫 return 語句。可選值有:

  • always--強制始終用大括號
  • as-needed--當大括號是可以省略的,強制不使用它們
  • never--禁止在函式體周圍出現大括號

arrow-parens:要求箭頭函式的引數使用圓括號。箭頭函式體只有一個引數時,可以省略圓括號。其它任何情況,引數都應被圓括號括起來。該規則強制箭頭函式中圓括號的使用的一致性。可選值有:

  • always--要求在所有情況下使用圓括號將引數括起來。
  • as-needed--當只有一個引數時允許省略圓括號。

constructor-super:要求在建構函式中有 super() 的呼叫。派生類中的建構函式必須呼叫 super()。非派生類的建構函式不能呼叫 super(), 否則 JavaScript 引擎將引發一個執行時錯誤。

no-class-assign:禁止修改類宣告的變數。大多數情況下,class A {}; A = 0;這樣的修改是個錯誤。

no-const-assign:禁止修改 const 宣告的變數。

no-dupe-class-members:禁止類成員中出現重複的名稱。如果類成員中有同名的宣告,最後一個宣告將會默默地覆蓋其它宣告,它可能導致意外的行為。

no-duplicate-imports:禁止重複模組匯入。為每個模組使用單一的 import 語句會是程式碼更加清新,因為你會看到從該模組匯入的所有內容都在同一行。import { A } from 'module'; import { B } from 'module'; 應該合併為 import { A, B } from 'module'; 會使匯入列表更加簡潔。

no-new-symbol:禁止 Symbolnew 一起使用。Symbol 應該作為函式呼叫。

no-this-before-super:禁止在建構函式中,在呼叫 super() 之前使用 thissuper。在派生類的建構函式中,如果在呼叫 super() 之前使用 thissuper,它將會引發一個引用錯誤。

no-useless-rename:禁止在 importexport 和解構賦值時將引用重新命名為相同的名字。ES2015 允許在 importexport 和解構賦值時對引用進行重新命名。引用有可能被重新命名成相同的名字。import { foo as foo } from "bar"; 這和沒有重新命名是等價的,因此這種操作完全冗餘。

no-var:要求使用 letconst 而不是 var。塊級作用域在很多其他程式語言中很普遍,能幫助程式設計師避免錯誤。

object-shorthand:要求或禁止物件字面量中方法和屬性使用簡寫語法。ECMAScript 6 提供了簡寫的形式去定義物件中的方法和屬性。你可以配置該規則來要求或禁止簡寫。

// 屬性
var foo = {
    x: x,
    y: y,
    z: z,
};
// 等效於
var foo = { x, y, z }

// 方法
var foo = {
    a: function() {},
    b: function() {}
};

//等效於
var foo = {
    a() {},
    b() {}
}
複製程式碼

prefer-const:要求使用 const 宣告那些宣告後不再被修改的變數。如果一個變數不會被重新賦值,最好使用 const 進行宣告。從而減少認知負荷,提高可維護性。

prefer-rest-params:要求使用剩餘引數而不是 arguments。剩餘引數得到的是真正的陣列,而 arguments是類陣列,沒有 Array.prototype 方法,有時候還需要再轉化一步。剩餘引數的語義更明確,即宣告的形參之外的實參會被歸進陣列。

prefer-spread:要求使用擴充套件運算子而非 .apply()。在 ES2015 之前,必須使用 Function.prototype.apply() 呼叫可變引數函式。如 var args = [1, 2, 3, 4]; Math.max.apply(Math, args);,在 ES2015 中,可以使用擴充套件運算子呼叫可變引數函式。var args = [1, 2, 3, 4]; Math.max(...args);

prefer-template:要求使用模板字面量而非字串連線。

require-yield:要求 generator 函式內有 yield

import/no-mutable-exports:禁止 export 暴露可更改的資料。也就是說 export 出的必須用 const 定義,如:const name = 'a'; export default name;

未完待續 歡迎大家補充……

相關文章