JavaScript 基礎入門05
嚴格模式
運行js代碼的過程中,除了正常的代碼運行模式以外,還存在嚴格模式(strict mode)。意義在於讓代碼采用更加嚴格的JavaScript語法。
需要注意的是,相同的代碼,在不同的模式下可能會有不同的結果。很多時候在正常模式下能夠運行的代碼在嚴格模式下未必能夠運行。
嚴格模式的設計目的
早期的 JavaScript 語言有很多設計不合理的地方,但是為了兼容以前的代碼,又不能改變老的語法,只能不斷添加新的語法,引導程序員使用新語法。
嚴格模式是從 ES5 進入標准的,主要目的有以下幾個。
- 明確禁止一些不合理、不嚴謹的語法,減少 JavaScript 語言的一些怪異行為。
- 增加更多報錯的場合,消除代碼運行的一些不安全之處,保證代碼運行的安全。
- 提高編譯器效率,增加運行速度。
- 為未來新版本的 JavaScript 語法做好鋪墊。
總之,嚴格模式體現了 JavaScript 更合理、更安全、更嚴謹的發展方向。
如何開啟使用嚴格模式
在js代碼中想要開啟嚴格模式,需要通過一行字符串來開啟:
'use strict';
需要注意的是,老版本的引擎會把它當作一行普普通通的字符串,並且忽略。而新版本的瀏覽器引擎可以很好的支持嚴格模式。
嚴格模式即可以用於當前的整個腳本,也可以用於單個函數。
1、嚴格模式應用於整個腳本文件
use strict放在腳本文件的第一行,整個腳本都將以嚴格模式運行。如果這行語句不在第一行就無效,整個腳本會以正常模式運行。(嚴格地說,只要前面不是產生實際運行結果的語句,use strict可以不在第一行,比如直接跟在一個空的分號后面,或者跟在注釋后面。)
<script>
'use strict';
console.log('這是嚴格模式');
</script>
<script>
console.log('這是正常模式');
</script>
在上面的代碼中,一個網頁文件中有兩段JavaScript代碼。前面的script標簽里面開啟了嚴格模式,后面的script標簽里面則沒有開啟嚴格模式。
如果use strict寫成下面這樣,則不起作用,嚴格模式必須從代碼一開始就生效。
<script>
console.log('這是正常模式');
'use strict';
</script>
2、嚴格模式應用在單個函數
use strict放在函數當中的第一行,則整個函數以嚴格模式來運行。
function strict() {
'use strict';
return '這是嚴格模式';
}
function strict2() {
'use strict';
function f() {
return '這也是嚴格模式';
}
return f();
}
function notStrict() {
return '這是正常模式';
}
有時,需要把不同的腳本合並在一個文件里面。如果一個腳本是嚴格模式,另一個腳本不是,它們的合並就可能出錯。嚴格模式的腳本在前,則合並后的腳本都是嚴格模式;如果正常模式的腳本在前,則合並后的腳本都是正常模式。這兩種情況下,合並后的結果都是不正確的。這時可以考慮把整個腳本文件放在一個立即執行的匿名函數之中。
(function () {
'use strict';
// some code here
})();
顯式報錯
嚴格模式使得js的語法變得更加的嚴格。更多的操作都會顯式的報錯。在這其中,有一些操作,在正常模式下只會默默的失敗,但是不會報錯。
1、只讀屬性不可寫
嚴格模式下,設置字符串的length屬性,會報錯。
'use strict';
'abc'.length = 5;
// TypeError: Cannot assign to read only property 'length' of string 'abc'
上面代碼報錯,因為length是只讀屬性,嚴格模式下不可寫。正常模式下,改變length屬性是無效的,但不會報錯。
嚴格模式下,對只讀屬性賦值,或者刪除不可配置(non-configurable)屬性都會報錯。
// 對只讀屬性賦值會報錯
'use strict';
Object.defineProperty({}, 'a', {
value: 37,
writable: false
});
obj.a = 123;
// TypeError: Cannot assign to read only property 'a' of object #<Object>
// 刪除不可配置的屬性會報錯
'use strict';
var obj = Object.defineProperty({}, 'p', {
value: 1,
configurable: false
});
delete obj.p
// TypeError: Cannot delete property 'p' of #<Object>
2、禁止擴展的對象不可擴展。
嚴格模式下,對禁止擴展的對象添加新屬性,會報錯。
'use strict';
var obj = {};
Object.preventExtensions(obj);
obj.v = 1;
// Uncaught TypeError: Cannot add property v, object is not extensible
上面代碼中,obj對象禁止擴展,添加屬性就會報錯。
3、eval 、 arguments不可作為標識名
嚴格模式下,使用eval或者arguments作為標識名,將會報錯。下面的語句都會報錯。
'use strict';
var eval = 17;
var arguments = 17;
var obj = { set p(arguments) { } };
try { } catch (arguments) { }
function x(eval) { }
function arguments() { }
var y = function eval() { };
var f = new Function('arguments', "'use strict'; return 17;");
// SyntaxError: Unexpected eval or arguments in strict mode
4、函數不能存在重名的參數
正常模式下,如果函數有多個重名的參數,可以用arguments[i]讀取。嚴格模式下,這屬於語法錯誤。
function f(a, a, b) {
'use strict';
return a + b;
}
// Uncaught SyntaxError: Duplicate parameter name not allowed in this context
5、進制八進制的前綴0表示法
正常模式下,整數的第一位如果是0,表示是八進制數,比如0100等於十進制的64。
在嚴格模式下則進制使用這種表示法,整數第一位是0,將會報錯。
'use strict';
var n = 0100;
// Uncaught SyntaxError: Octal literals are not allowed in strict mode.
6、 全局變量顯式的聲明
正常模式當中,如果一個變量沒有聲明就賦值,默認是全局變量。嚴格模式下禁止使用這種語法,全局變量必須顯式的聲明。
'use strict';
v = 1; // 報錯,v未聲明
for (i = 0; i < 2; i++) { // 報錯,i 未聲明
// ...
}
function f() {
x = 123;
}
f() // 報錯,未聲明就創建一個全局變量
因此,嚴格模式下,變量都必須先聲明,然后再使用。
7、嚴格模式下禁止this關鍵字指向全局對象
正常模式下,函數內部的this可能會指向全局對象,嚴格模式禁止這種用法,避免無意間創造全局變量。
// 正常模式
function f() {
console.log(this === window);
}
f() // true
// 嚴格模式
function f() {
'use strict';
console.log(this === undefined);
}
f() // true
上面代碼中,嚴格模式的函數體內部this是undefined。
這種限制對於構造函數尤其有用。使用構造函數時,有時忘了加new,這時this不再指向全局對象,而是報錯。
function f() {
'use strict';
this.a = 1;
};
f();// 報錯,this 未定義
嚴格模式下,函數直接調用時(不使用new調用),函數內部的this表示undefined(未定義),因此可以用call、apply和bind方法,將任意值綁定在this上面。正常模式下,this指向全局對象,如果綁定的值是非對象,將被自動轉為對象再綁定上去,而null和undefined這兩個無法轉成對象的值,將被忽略。
// 正常模式
function fun() {
return this;
}
fun() // window
fun.call(2) // Number {2}
fun.call(true) // Boolean {true}
fun.call(null) // window
fun.call(undefined) // window
// 嚴格模式
'use strict';
function fun() {
return this;
}
fun() //undefined
fun.call(2) // 2
fun.call(true) // true
fun.call(null) // null
fun.call(undefined) // undefined
上面代碼中,可以把任意類型的值,綁定在this上面。
8、嚴格模式下禁止使用fn.callee、fn.caller
在嚴格模式下,函數體內禁止使用fn.caller、fn.arguments,否則會報錯。
這意味着在函數體內不能調用棧了。
function f1() {
'use strict';
f1.caller; // 報錯
f1.arguments; // 報錯
}
f1();
9、禁止刪除變量
嚴格模式下無法刪除變量,如果使用delete命令刪除一個變量,會報錯。只有對象的屬性,且屬性的描述對象的configurable屬性設置為true,才能被delete命令刪除。
'use strict';
var x;
delete x; // 語法錯誤
var obj = Object.create(null, {
x: {
value: 1,
configurable: true
}
});
delete obj.x; // 刪除成功
10、靜態綁定
JavaScript 語言的一個特點,就是允許“動態綁定”,即某些屬性和方法到底屬於哪一個對象,不是在編譯時確定的,而是在運行時(runtime)確定的。
嚴格模式對動態綁定做了一些限制。某些情況下,只允許靜態綁定。也就是說,屬性和方法到底歸屬哪個對象,必須在編譯階段就確定。這樣做有利於編譯效率的提高,也使得代碼更容易閱讀,更少出現意外。
在嚴格模式下是禁止使用with語句的。
在正常的模式下,JavaScript語言有兩種變量作用域(scope):全局作用域和函數作用域。
在嚴格模式下多出了第三種作用域:eval作用域。
在正常模式下,eval語句的作用域,取決於它處於全局作用域,還是函數作用域。嚴格模式下,eval語句本身就是一個作用域,不能夠在其所運行的作用域創建新的變量了,簡單的說,eval所生成的變量只能用於eval內部。
(function () {
'use strict';
var x = 2;
console.log(eval('var x = 5; x')) // 5
console.log(x) // 2
})()
上面代碼中,由於eval語句內部是一個獨立作用域,所以內部的變量x不會泄露到外部。
注意,如果希望eval語句也使用嚴格模式,有兩種方式。
// 方式一
function f1(str){
'use strict';
return eval(str);
}
f1('undeclared_variable = 1'); // 報錯
// 方式二
function f2(str){
return eval(str);
}
f2('"use strict";undeclared_variable = 1') // 報錯
上面兩種寫法,eval內部使用的都是嚴格模式。
11、arguments不再追蹤參數的變化
變量arguments代表函數的參數。嚴格模式下,函數內部改變參數與arguments的聯系被切斷了,兩者不再存在聯動關系。
function f(a) {
a = 2;
return [a, arguments[0]];
}
f(1); // 正常模式為[2, 2]
function f(a) {
'use strict';
a = 2;
return [a, arguments[0]];
}
f(1); // 嚴格模式為[2, 1]
上面代碼中,改變函數的參數,不會反應到arguments對象上來。
12、嚴格模式表示向下一個版本過渡
JavaScript 語言的下一個版本是 ECMAScript 6,為了平穩過渡,嚴格模式引入了一些 ES6 語法。
ES6 會引入塊級作用域。為了與新版本接軌,ES5 的嚴格模式只允許在全局作用域或函數作用域聲明函數。也就是說,不允許在非函數的代碼塊內聲明函數。
'use strict';
if (true) {
function f1() { } // 語法錯誤
}
for (var i = 0; i < 5; i++) {
function f2() { } // 語法錯誤
}
上面代碼在if代碼塊和for代碼塊中聲明了函數,ES5 環境會報錯。
注意,如果是 ES6 環境,上面的代碼不會報錯,因為 ES6 允許在代碼塊之中聲明函數。
13、保留字
為了向將來 JavaScript 的新版本過渡,嚴格模式新增了一些保留字(implements、interface、let、package、private、protected、public、static、yield等)。使用這些詞作為變量名將會報錯。
function package(protected) { // 語法錯誤
'use strict';
var implements; // 語法錯誤
}
字符串
字符串的創建
字符串(String)對象是JavaScript原生三大包裝對象之一,用來生成字符串對象。
var s1 = 'abc';
var s2 = new String('abc');
typeof s1 // "string"
typeof s2 // "object"
s2.valueOf() // "abc"
在上面的代碼中,變量s1是字符串,s2是對象。由於s2是字符串對象s2.valueOf方法返回的就是它所對應的原始字符串。
字符串對象是一個類似數組的對象(很像數組,但不是數組)。
new String('abc')
// String {0: "a", 1: "b", 2: "c", length: 3}
(new String('abc'))[1] // "b"
上面代碼中,字符串abc對應的字符串對象,有數值鍵(0、1、2)和length屬性,所以可以像數組那樣取值。
除了用作構造函數,String對象還可以當作工具方法使用,將任意類型的值轉為字符串。
String(true) // "true"
String(5) // "5"
上面代碼將布爾值true和數值5,分別轉換為字符串。
字符串實例方法之常用API
1、 String.prototype.charAt()
charAt方法返回指定位置的字符,參數是從0開始編號的位置。
var str = "hello,world";
console.log(str.charAt(1)); // e
這個方法我們可以使用數組下標來代替。
var str = "hello,world";
console.log(str[1]);//e
需要注意的是,如果參數為負數,或者大於等於字符串的長度,
charAt返回空字符串。
2、String.prototype.slice()
slice方法用於從原字符串取出子字符串並返回,不改變原字符串。它的第一個參數是子字符串的開始位置,第二個參數是子字符串的結束位置(不含該位置)。
'JavaScript'.slice(0, 4) // "Java"
如果省略第二個參數,則表示子字符串一直到原字符串結束。
'JavaScript'.slice(4) // "Script"
如果參數是負值,表示從結尾開始倒數計算的位置,即該負值加上字符串長度。
'JavaScript'.slice(-6) // "Script"
'JavaScript'.slice(0, -6) // "Java"
'JavaScript'.slice(-2, -1) // "p"
如果第一個參數大於第二個參數,slice方法返回一個空字符串。
'JavaScript'.slice(2, 1) // ""
3、String.prototype.substring()
substring方法用於從原字符串取出子字符串並返回,不改變原字符串,跟slice方法很相像。它的第一個參數表示子字符串的開始位置,第二個位置表示結束位置(返回結果不含該位置)。
'JavaScript'.substring(0, 4) // "Java"
如果省略第二個參數,則表示子字符串一直到原字符串的結束。
'JavaScript'.substring(4) // "Script"
如果第一個參數大於第二個參數,substring方法會自動更換兩個參數的位置。
'JavaScript'.substring(10, 4) // "Script"
// 等同於
'JavaScript'.substring(4, 10) // "Script"
上面代碼中,調換substring方法的兩個參數,都得到同樣的結果。
如果參數是負數,substring方法會自動將負數轉為0。
'JavaScript'.substring(-3) // "JavaScript"
'JavaScript'.substring(4, -3) // "Java"
上面代碼中,第二個例子的參數-3會自動變成0,等同於'JavaScript'.substring(4, 0)。由於第二個參數小於第一個參數,會自動互換位置,所以返回Java。
由於這些規則違反直覺,因此不建議使用substring方法,應該優先使用slice。
4、String.prototype.indexOf() ,String.prototype.lastIndexOf()
indexOf方法用於確定一個字符串在另一個字符串中第一次出現的位置,返回的結果是匹配開始的位置。如果返回-1,就表示不匹配。
'hello world'.indexOf('o') // 4
'JavaScript'.indexOf('script') // -1
indexOf方法還可以接受第二個參數,表示從該位置開始向后匹配。
'hello world'.indexOf('o', 6) // 7
lastIndexOf方法的用法跟indexOf方法一致,主要的區別是lastIndexOf從尾部開始匹配,indexOf則是從頭部開始匹配。
'hello world'.lastIndexOf('o') // 7
另外,lastIndexOf的第二個參數表示從該位置起向前匹配。
'hello world'.lastIndexOf('o', 6) // 4
5、String.prototype.search(),String.prototype.replace()
search方法的用法基本等於match,但是返回值為匹配的第一個位置。如果沒有找到匹配,則返回-1。
'cat, bat, sat, fat'.search('at') // 1
search方法還可以使用正則表達式作為參數。
replace方法用於替換匹配的子字符串,一般情況下只替換第一個匹配(除非使用帶有g修飾符的正則表達式)。
'aaa'.replace('a', 'b') // "baa"
replace方法還可以使用正則表達式作為參數.
6、String.prototype.split()
split方法按照給定規則分割字符串,返回一個由分割出來的子字符串組成的數組。
'a|b|c'.split('|') // ["a", "b", "c"]
如果分割規則為空字符串,則返回數組的成員是原字符串的每一個字符。
'a|b|c'.split('') // ["a", "|", "b", "|", "c"]
如果省略參數,則返回數組的唯一成員就是原字符串。
'a|b|c'.split() // ["a|b|c"]
如果滿足分割規則的兩個部分緊鄰着(即兩個分割符中間沒有其他字符),則返回數組之中會有一個空字符串。
'a||c'.split('|') // ['a', '', 'c']
如果滿足分割規則的部分處於字符串的開頭或結尾(即它的前面或后面沒有其他字符),則返回數組的第一個或最后一個成員是一個空字符串。
'|b|c'.split('|') // ["", "b", "c"]
'a|b|'.split('|') // ["a", "b", ""]
split方法還可以接受第二個參數,限定返回數組的最大成員數。
'a|b|c'.split('|', 0) // []
'a|b|c'.split('|', 1) // ["a"]
'a|b|c'.split('|', 2) // ["a", "b"]
'a|b|c'.split('|', 3) // ["a", "b", "c"]
'a|b|c'.split('|', 4) // ["a", "b", "c"]
上面代碼中,split方法的第二個參數,決定了返回數組的成員數。
split方法還可以使用正則表達式作為參數.
7、String.prototype.charCodeAt()
charCodeAt方法返回字符串指定位置的 Unicode 碼點(十進制表示),相當於String.fromCharCode()的逆操作。
'abc'.charCodeAt(1) // 98
上面代碼中,abc的1號位置的字符是b,它的 Unicode 碼點是98。
如果沒有任何參數,charCodeAt返回首字符的 Unicode 碼點。
'abc'.charCodeAt() // 97
如果參數為負數,或大於等於字符串的長度,charCodeAt返回NaN。
'abc'.charCodeAt(-1) // NaN
'abc'.charCodeAt(4) // NaN
注意,charCodeAt方法返回的 Unicode 碼點不會大於65536(0xFFFF),也就是說,只返回兩個字節的字符的碼點。如果遇到碼點大於 65536 的字符(四個字節的字符),必需連續使用兩次charCodeAt,不僅讀入charCodeAt(i),還要讀入charCodeAt(i+1),將兩個值放在一起,才能得到准確的字符。
8、String.prototype.concat()
concat方法用於連接兩個字符串,返回一個新字符串,不改變原字符串。
var s1 = 'abc';
var s2 = 'def';
s1.concat(s2) // "abcdef"
s1 // "abc"
該方法可以接受多個參數。
'a'.concat('b', 'c') // "abc"
如果參數不是字符串,concat方法會將其先轉為字符串,然后再連接。
var one = 1;
var two = 2;
var three = '3';
''.concat(one, two, three) // "123"
one + two + three // "33"
上面代碼中,concat方法將參數先轉成字符串再連接,所以返回的是一個三個字符的字符串。作為對比,加號運算符在兩個運算數都是數值時,不會轉換類型,所以返回的是一個兩個字符的字符串。
9、String.prototype.trim()
trim方法用於去除字符串兩端的空格,返回一個新字符串,不改變原字符串。
' hello world '.trim()
// "hello world"
該方法去除的不僅是空格,還包括制表符(\t、\v)、換行符(\n)和回車符(\r)。
'\r\nabc \t'.trim() // 'abc'
10、String.prototype.toLowerCase(),String.prototype.toUpperCase()
toLowerCase方法用於將一個字符串全部轉為小寫,toUpperCase則是全部轉為大寫。它們都返回一個新字符串,不改變原字符串。
'Hello World'.toLowerCase()
// "hello world"
'Hello World'.toUpperCase()
// "HELLO WORLD"
11、String.prototype.match()
match方法用於確定原字符串是否匹配某個子字符串,返回一個數組,成員為匹配的第一個字符串。如果沒有找到匹配,則返回null。
'cat, bat, sat, fat'.match('at') // ["at"]
'cat, bat, sat, fat'.match('xt') // null
返回的數組還有index屬性和input屬性,分別表示匹配字符串開始的位置和原始字符串。
var matches = 'cat, bat, sat, fat'.match('at');
matches.index // 1
matches.input // "cat, bat, sat, fat"
小練習:
留言過濾:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>留言過濾</h1>
<textarea id="txt1" cols="30" rows="10"></textarea>
<br>
<input type="button" id="btn1" value="點擊過濾"> <br>
<textarea id="txt2" cols="30" rows="10"></textarea>
</body>
<script>
// 獲取元素
var oTxt1 = document.getElementById("txt1");
var oTxt2 = document.getElementById("txt2");
var btn = document.getElementById("btn1");
btn.onclick = function () {
// alert(123)
var re = /DDS|fuck|FUCK|DSD/g;
oTxt2.value = oTxt1.value.replace(re,"***");
};
</script>
</html>
簡單的注冊驗證:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>簡易版注冊</h1>
<hr>
<form action="" name="myform">
<table>
<tr>
<td>用戶名:</td>
<td><input type="text" name="username" onblur="checkUser()"></td>
<td id="username_info"></td>
</tr>
<tr>
<td>郵箱:</td>
<td><input type="email" name="email" onblur="checkEmail()"></td>
<td id="email_info"></td>
</tr>
<tr>
<td>密碼:</td>
<td><input type="password" name="pwd" onblur="checkPwd()"></td>
<td id="pwd_info"></td>
</tr>
<tr>
<td>確認密碼:</td>
<td><input type="password" name="repwd" onblur="checkRePwd()"></td>
<td id="repwd_info"></td>
</tr>
<tr>
<td></td>
<td><input type="submit" value="提交數據"></td>
<td></td>
</tr>
</table>
</form>
</body>
<script>
// 驗證用戶名
function checkUser() {
// 獲取用戶輸入
var value = document.myform.username.value;
// 獲取id
var td = document.getElementById('username_info');
// 使用正則進行驗證
if(value.search(/^\w{6,12}$/) === -1){
td.innerHTML = "用戶名必須是6-12位數字、字母、下划線";
td.style.color = "red";
}else {
td.innerHTML = "用戶名可用";
td.style.color = "green";
}
}
// 驗證郵箱
function checkEmail() {
// 獲取用戶輸入
var value = document.myform.email.value;
// 獲取id
var td = document.getElementById('email_info');
// 使用正則進行驗證
if(value.search(/^\w+@\w+(\.\w+){1,3}$/) === -1){
td.innerHTML = "郵箱格式不正確";
td.style.color = "red";
}else {
td.innerHTML = "郵箱可用";
td.style.color = "green";
}
}
// 驗證密碼
function checkPwd() {
// 獲取用戶輸入
var pwd = document.myform.pwd.value;
// 獲取id
var td = document.getElementById('pwd_info');
// 使用正則進行驗證
if(pwd.length < 6 || pwd.length > 18){
td.innerHTML = "密碼長度必須是6-18位";
td.style.color = "red";
}else {
td.innerHTML = "密碼可用";
td.style.color = "green";
}
}
// 驗證第二次輸入的密碼
function checkRePwd() {
// 獲取用戶輸入
var pwd = document.myform.pwd.value;
var repwd = document.myform.repwd.value;
// 獲取id
var td = document.getElementById('repwd_info');
if(pwd !== repwd){
td.innerHTML = "兩次密碼不一致";
td.style.color = "red";
}else{
td.innerHTML = "輸入正確";
td.style.color = "green";
}
}
</script>
</html>
統計字符串中每個字符出現的次數
// 創建一個字符串和一個對象
var str = "adminhelloworldadn",obj = {};
// 開啟循環進行處理
for(var i=0;i<str.length;i++){
obj[str[i]] = (obj[str[i]] + 1) || 1;
}
console.log(obj); // 打印出來是一個對象的形式
// 為了便於閱讀,轉換成json
var json = JSON.stringify(obj);
console.log(json); // {"a":2,"d":3,"m":1,"i":1,"n":2,"h":1,"e":1,"l":3,"o":2,"w":1,"r":1}
