基本語法
1.嚴格模式 "use strict"
作用
- 消除JS語法的一些不合理、不嚴謹、不安全的問題,減少怪異行為並保證代碼運行安全
- 提高編譯器解釋器效率,增加運行速度
與標準模式的區別
- 隱式聲明或定義變量:嚴格模式不能通過省略
var關鍵字隱式聲明變量。會報引用錯誤“ReferenceError:abc is not define” - 對象重名的屬性:嚴格模式下對象不允許有重名的屬性。
var obj = {a:1, b:2, a:3}會報語法錯誤“SyntaxError”。 arguments.callee:通常我們使用這個語法來實現匿名函數的遞歸,但在嚴格模式下是不允許的。會報錯TypeError。with語句:嚴格模式下with語句是被禁用的。會報語法錯誤 SyntaxError 。
2.註釋/* */不可嵌套
類型系統
3.基本類型(標準類型)
主要介紹6種基本類型(Undefine、Null、Boolean、Number、String、Object)、原生類型及引用類型概念
原始數據類型:Undefined、Null、Boolean、String、Number
值存於棧內存(stack)中。佔據的空間小,大小固定,頻繁被使用。
引用數據類型:Object
值存於堆內存(heap)中。棧中只保留了一個指針,指向堆中存儲位置的起始地址。佔據空間大,大小不固定。
4.Undefined
出現場景
- 已聲明未賦值的變量
- 獲取對象不存在的屬性
- 無返回值的函數的執行結果
- 函數的參數沒有傳入
- void(expression) 常用於
<a href="javascript:void(0)">
向其他數據類型轉換
Boolean:false
Number:NaN
String:"undefined"
5.Null
出現場景
- null表示對象不存在 document.getElementById("notExitElement");
向其他數據類型轉換
Boolean:false
Number:0
String:"null"
6.Boolean : true false
出現場景
- 條件語句導致系統執行的隱式類型轉換
if(document.getElementById("notExitElement");){...} - 字面量或變量定義:
true,var a = true;
只有以下6個值會被轉換為false:
undefined
null
false
0
NaN
""或''(空字符串)
注意:空數組[]和空對象{},對應的布爾值都是true。
向其他數據類型轉換
Number:1 0
String:"true" "false"
7.String
出現場景
"abbv" 'lll'
向其他數據類型轉換
String: "" "123" "notEmpty"
Number: 0 123 NaN
Boolean:false true true
8.Number
出現場景
123var a = 1;
向其他數據類型轉換
Number: 0 123 NaN Infinity
Boolean:false true true false
String: "0" "123" "NaN" "Infinity"
十進制:沒有前導0的數值。
八進制:有前綴0o或0O的數值,或者有前導0、且只用到0-7的七個阿拉伯數字的數值。
十六進制:有前綴0x或0X的數值。
二進制:有前綴0b或0B的數值。
9.Object
一組屬性的集合。
出現場景
{a: "value-a", b: "value-b", ...}
向其他數據類型轉換
Object: {}
Number: NaN
Boolean: true
String: "[object Object]"
類型識別
主要介紹typeof、Object.prototype.toString、constructor、instanceof等類型識別的方法。
10.typeof
對標準數據類型的識別
typeof 1 // number
typeof true // boolean
typeof "str" // string
typeof undefined // undefined
typeof null // object
typeof {} // object
也就是説,標準數據類型中,除了null被識別為object,其餘類型都識別正確。
另外,需要注意typeof undefined 會得到 undefined,利用這一點,typeof可用於檢查一個沒有聲明的變量而不報錯:
if(a) { ... }
//Uncaught ReferenceError: a is not defined
if(typeof a === 'undefined') { console.log('未定義也不報錯') }
//未定義也不報錯
對具體對象的識別
typeof function(){} // function
typeof [] // object
typeof new Date(); // object
typeof /\d/; // object
function Person() {};
typeof new Person; // object
因此,不能識別具體的對象類型,函數對象除外。
11.Object.prototype.toString
Object.prototype.toString.call(1); // "[object Number]"
所以我們寫一個函數來簡化調用,以及截取我們需要的部分:
function type(obj){
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
}
type(1) // number
type("str") // string
type(true) // boolean
type(undefined) //undefined
type(null) //null
type({}) //object
type([]) //array
type(new Date) // date
type(/\d/) // regexp
type(function(){}) // function
Object.prototype.toString可以準確識別出所有的標準類型以及內置(build-in)對象類型。
那麼自定義類型呢?
function Point(x, y){
this.x = x;
this.y = y;
}
type(new Point(1, 2)) //object
所以它無法識別出自定義類型。
12.constructor
對象原型上的一個屬性,它指向了構造器本身。
可以識別原始數據類型(undefined和null除外,因為它們沒有構造器),內置對象類型和自定義對象類型,尤其注意自定義對象類型(如:Person)。
//判斷原始類型
"Jerry".constructor === String //true
(1).constructor === Number //true
true.constructor === Boolean //true
({}).constructor === Object //true
//判斷內置對象
[].constructor === Array //true
//判斷自定義對象 ヾ(o◕∀◕)ノ
function Person(name){
this.name = name;
}
new Person("Jerry").constructor === Person // true ヾ(o◕∀◕)ノ !!!
13.instanceof
//判斷原始類型
1 instanceof Number // false
true instanceof boolean // false
//判斷內置對象類型
[] instanceof Array // true
/\b/ instanceof RegExp // true
//判斷自定義對象類型
function Person(name){
this.name = name;
}
new Person("miao") instanceof Person // true
不能判斷原始類型,可以判斷內置對象類型,可以判斷自定義對象類型及父子類型。
內置對象
分為兩類:普通對象與構造器對象。其中,構造器對象可用於實例化普通對象。
普通對象只有自身的屬性和方法。
構造器對象除了自身的屬性和方法之外,還有原型對象prototype上的屬性和方法,以及實例化出來的對象的屬性和方法。
以下代碼創建了一個Point構造器並實例化了一個p對象
function Point(x, y){
this.x = x;
this.y = y;
}
Point.prototype.move = function(x, y){
this.x += x;
this.y += y;
}
var p = new Point(1,1);
p.move(1,2);
普通對象p的原型鏈:
"__proto__"就是我們通常説的原型鏈屬性,他有如下幾個特點:
- "__proto__"是對象的一個內部隱藏屬性。
- "__proto__"是對實例化該對象的構造器的
prototype屬性的一個引用,因此可以訪問prototype的所有屬性和方法。- 除了Object對象,每個對象都有一個"__proto__"屬性,"__proto__"逐級增長形成一個鏈就是我們所説的原型鏈,原型鏈頂端是一個Object對象。
- 當開發者調用對象屬性或方法時(比如"p.move(1,2)"),引擎首先會查找p對象的自身屬性,如果自身屬性中沒有"move"方法,則會繼續沿原型鏈逐級向上查找,直到找到該方法並調用。
- "__proto__"跟瀏覽器引擎實現相關,不同的引擎中名字和實現不盡相同(chrome、firefox中名稱是"__proto__",並且可以被訪問到,IE中無法訪問)。基於代碼兼容性、可讀性等方面的考慮,不建議開發者顯式訪問"__proto__"屬性或通過"__proto__"更改原型鏈上的屬性和方法,可以通過更改構造器prototype對象來更改對象的__proto__屬性。
構造器對象的原型鏈:
Point.prototype其實就是個普通對象:
Point.prototype = {
move: function(){ ... },
constructor: function(){ ... }
}
構造器對象相對於普通對象有如下幾個特點:
1.構造器對象原型鏈上倒數第二個__proto__是一個Function.prototype對象引用,因此可以調用Function.prototype的屬性和方法。
2.構造器對象本身有一個prototype屬性,有這個屬性就表明該對象可以用來生成其他對象:用該構造器實例化對象時該prototype會被實例對象的__proto__所引用,即添加到實例對象的原型鏈上。
3.構造器對象本身是一個function對象,因此會有name,length等自身屬性。
14.Object對象
對象的所有鍵名都是字符串,鍵名加不加引號都可以。
用於創建對象的下面三行語句是等價的:
var o1 = {};
var o2 = new Object();
var o3 = Object.create(Object.prototype);
String/Number/Boolean/Array/Date/Error構造器都是Object子類對象。
自身的屬性、方法:prototype,create,keys,getOwnPropertyNames,getOwnPropertyDescriptor,getPrototypeOf
原型對象prototype的屬性和方法:constructor,toString,valueOf,hasOwnProperty,isPrototypeOf
生成的實例對象只有原型鏈屬性__proto__,沒有其他的屬性,也就是Object沒有實例對象屬性,方法。相比較而言,Array有實例對象屬性length,也就是説,Arrya類型的實例對象有length屬性。
幾個重要方法:
Object.create(proto) : 基於原型對象創建新對象,傳入的是一個對象,會被作為創建出的對象的__proto__屬性值。
Object.keys(obj):返回一個由obj自身所擁有的可枚舉屬性的屬性名組成的數組。比如:
var obj = { a: 'aaa', b: 'bbb'};
Object.keys(obj); // ["a", "b"]
var arr = ['a', 'b', 'c'];
Object.keys(arr); // ["0", "1", "2"]
如果想要對象上不可枚舉的屬性也被列舉出來,用Object.getOwnPropertyNames(obj)方法。比如:
var arr = ['a', 'b', 'c'];
Object.getOwnPropertyNames(arr); // ["0", "1", "2", "length"]
Object.getOwnPropertyDescriptor(obj, 'prop') 可以讀出對象自身屬性的屬性描述對象。
var obj = { prop1: 'hello'};
Object.getOwnPropertyDescriptor(obj, 'prop1');
//Object {value: "a", writable: true, enumerable: true, configurable: true}
Object.getPrototypeOf(obj)獲取對象的Prototype對象。
Object.prototype.valueOf :返回指定對象的原始值
這個方法幾乎不會手動調用,而是JavaScript自動去調用將一個對象轉換成原始值(primitive value)。
默認情況下,每一個內置對象都會覆蓋這個方法返回一個合理的值。如果對象沒有原始值,這個方法就會返回對象自身。比如:
//有原始值
var num= new Number(3);
num; // {[[PrimitiveValue]]: 3}
num.valueOf(); // 3
num + 4; // 7 valueOf()被JavaScript隱式調用了
//沒有原始值
var obj = {'a': 'aaa'}
obj.valueOf(); // {'a': 'aaa'}
假設你有一個對象類型的myNumberType,你想為它創建一個valueOf方法。下面的代碼可以自定義一個valueOf方法:
function myNumberType (n){
this.number = n;
}
myNumberType.prototype.valueOf = function () {
return this.number;
}
var obj = new myNumberType(4);
obj + 3; // 7 自定義的valueOf方法被JavaScript自動調用了
Object.prototype.toString :獲取方法調用者的標準類型
默認情況下,toString() 方法被每個繼承自Object的對象繼承。如果此方法在自定義對象中未被覆蓋,toString() 返回 "[object type]",其中type是對象類型。以下代碼説明了這一點:
var obj = {a: 1};
obj.toString(); // "[object Object]"
很多內置對象重寫了toString方法。包括Array,Boolean,Number,Date,String例如:
var a = new Number(1)
b = a.toString(); // "1"
//可用於進行進制轉換
var num1 = 10;
num1.toString(16); // "a"
num1.toString(2); // "1010"
num1.toString(8); // "12"
var c = new String('abc')
d = c.toString(); // "abc"
var e = new Array(['a', 'b', 'c'])
f = e.toString(); // "a,b,c"
還可以用來檢查對象類型:
var toString = Object.prototype.toString;
toString.call(new Date); // [object Date]
toString.call(new String); // [object String]
toString.call(Math); // [object Math]
toString.call(1); // [object Number]
toString.call('1'); // [object String]
//Since JavaScript 1.8.5
toString.call(undefined); // [object Undefined]
toString.call(null); // [object Null]
Object.prototype.hasOwnProperty :判斷一個屬性是對象自身屬性,還是原型上的屬性。
var obj = Object.create({a:1});
obj.b = 2;
obj.hasOwnProperty('a'); //false
obj.hasOwnProperty('b'); //true
Object.property.isPrototypeOf:判斷當前對象是否為另一個對象的原型。
var father = { a: 'aaa', b: 'bbb'};
var child = Object.create(father);
father.isPrototypeOf(child); // true
delete命令用於刪除對象的屬性,不管該屬性是否存在,刪除後都返回true,只有當該屬性的configurable: false時,刪除操作才返回false,也就是該屬性不得刪除。
var o = { p: 'Hello'}
delete o.p;
o; // Object{}
注意:delete不得刪除var定義的變量。
in運算符用於檢查對象是否包含某個屬性(這裏寫的是屬性的鍵名)。存在返回true。可以用於判斷全局變量是否存在。
var o = {p: 'hello'}
'p' in o; // true
'o' in window; // true
存在的問題是:in運算符對於繼承的屬性也返回true。
for ... in循環用來遍歷一個對象的全部屬性。
var o = { a: 'hello', b: 'world'};
for( var key in o) {
console.log(key + ': ' + o[key]);
}
//a: hello
//b: world
注意:會遍歷繼承來的屬性。所以一般不推薦使用。一般會用Object.keys(obj),可以只遍歷對象本身的屬性。
15.String, Number, Boolean
使用String()和Number()方法可以將各種類型的值強制轉換為string或number類型。比如:
Number('222'); //222
Number(null); // 0
Number(undefined); // NaN
String(new Number(2)); // '2'
String(['a', 'b']); //'a,b'
但當轉換的數值為對象時,轉換規則為:
Number(obj):(簡單地説值為NaN)
1.先調用對象自身的valueOf()方法,如果返回原始類型的值,那麼直接對該值使用Number函數,不再進行後續步驟。
2.如果valueOf方法返回的值類型是對象,那麼調用這個對象的toString()方法,如果返回的是原始類型的值,那麼對這個值使用Number()方法,不再進行後續步驟。
3.如果toString返回的是對象,就報錯。
var obj = {a: 1};
Number(obj); // NaN
//等同於
if(typeof obj.valueOf() === 'object') {
Number(obj.toString());
} else {
Number(obj.valueOf());
}
String(obj):
1.先調用對象自身的toString()方法,如果返回原始類型的值,那麼直接對該值使用String函數,不再進行後續步驟。
2.如果toString方法返回的值類型是對象,那麼調用這個對象的valueOf()方法,如果返回的是原始類型的值,那麼對這個值使用String()方法,不再進行後續步驟。
3.如果valueOf返回的是對象,就報錯。
var obj = {a: 1}
String(obj); // [object Object]
//等價於
if(typeof obj.toString() === 'object') {
String(obj.valueOf());
} else {
String(obj.toString());
}
String
構造器對象屬性、方法:prototype, fromCharCode
原型對象屬性、方法:constructor,indexOf,replace,slice,charCodeAt,toLowerCase
幾個重要方法:
1.String.prototype.indexOf:獲取子字符串在字符串中位置索引(一次只查找一個)
語法:strObj.indexOf(searchvalue,fromindex)
var str = "abcdabcd";
var idx = str.indexOf("b"); // 1
var idx2 = str.indexOf("b", idx+1); //5
2.String.prototype.replace:查找字符串替換成目標字符(一次只替換一個)
語法:strObj.replace(regexp/searchvalue, newSubStr/function)
var str = "1 plus 1 equal 3";
str = str.replace("1","2"); // "2 plus 1 equal 3"
str = str.replace(/\d+/g, "$& dollar"); //"2 dollar plus 1 dollar equal 3 dollar"
第二個參數是newSubStr時,其中
$& 代表插入當前匹配的子串
$` 代表插入當前匹配的子串左邊的內容
$' 代表插入當前匹配的子串右邊的內容
$n 是當第一個參數為regexp對象,且n為小於100的非負整數時,插入regexp第n個括號匹配到的字符串
第二個參數是function時,函數的返回值為替換的字符串,注意如果第一個參數為regexp且為全局匹配,那麼每次匹配都會調用這個函數,把匹配到的值用函數的返回值替換。函數參數:
match:匹配的子串,相當於 $&
p1,p2,...: 如果第一個參數為regexp,對應第n個括號匹配到的字符串,相當於$1,$2
offset: 匹配到的字符串到原字符串中的偏移量,比如 bc 相對於 abcd ,offset 為1
string: 被匹配的原字符串
//精確的參數個數取決於第一個參數是否為regexp,以及有多少個括號分組
3.String.prototype.split:按分割符將字符串分割成字符串數組
語法:strObj.split(separator, howmany)
var str = "1 plus 1 equal 3";
str.split(" "); // ["1", "plus", "1", "equal", "3"]
str.split(" ", 3); // ["1", "plus", "1"]
str.split(/\d+/); //["", " plus ", " equal ", ""]
4.str.slice(beginSlice[, endSlice]) 提取一個字符串的一部分,並返回一個新的字符串
var str = 'abcdefg';
str.slice(1,3); // 'bc'
str; //'abcdefg'
5.str.substr(start[, length])返回從start開始,length個數的字符串
var str = 'abcdefg';
str.substr(1,3); //'bcd'
str; //'abcdefg'
6.str.substring(start[, end]) 返回從start開始到end(不包括end)結束的字符串。其中,start和end都大於0.
var str = 'abcdefg';
str.substring(1,3); //'bc'
str; //'abcdefg'
7.str.trim() 返回的是一個新字符串,刪除了原字符串兩端的空格
var str = ' abc ';
str.trim(); // 'abc'
str; // ' abc '
8.str.charAt(index) 從一個字符串中返回index位置的字符,indx默認是0
var str = 'abcdefg';
str.charAt(1); //'b'
9.str.concat(string2, string3[, ..., stringN])返回新字符串,將原字符串與一個或多個字符串相連
var str = 'abc';
var str2 = '123';
str.concat(str2);// 'abc123'
強烈建議使用 賦值操作符(+, +=)代替 concat 方法。
10.str.includes(searchString[, position])判斷searchString是否包含在str中,返回布爾值。
var str = 'abc';
str.includes('ab'); //true
11.str.indexOf(searchValue[, fromIndex]) 返回str中searchValue第一次出現的位置,沒找到返回-1
"Blue Whale".indexOf("Blute"); // returns -1
"Blue Whale".indexOf("Whale", 0); // returns 5
12.str.lastIndexOf(searchValue[, fromIndex]) 返回str中searchValue最後一次出現的位置,沒找到返回-1
"abcabc".lastIndexOf("a") // returns 3
"abcabc".lastIndexOf("d") // returns -1
"abcabc".lastIndexOf("a",2) // returns 0
"cbacba".lastIndexOf("a",0) // returns -1
13.str.endsWith(searchString [, position]);屬於ES6.判斷searchString是否是以另外一個給定的字符串結尾的,返回布爾值
var str = "To be, or not to be, that is the question.";
str.endsWith("question."); // true
str.endsWith("to be"); // false
str.endsWith("to be", 19); // true
str.endsWith("To be", 5); // true
14.str.match(regexp);當一個字符串與一個正則表達式匹配時,match()方法檢索匹配項。 返回一個包含了整個匹配結果以及任何括號捕獲的匹配結果的 Array ;如果沒有匹配項,則返回 null 。
如果正則表達式沒有 g 標誌,則 str.match() 會返回和 RegExp.exec() 相同的結果。
如果正則表達式包含 g 標誌,則該方法返回一個 Array ,它包含所有匹配的子字符串而不是匹配對象。
var str = 'For more information, see Chapter 3.4.5.1';
var re = /see (chapter \d+(\.\d)*)/i;
var found = str.match(re);
console.log(found);
// logs [ 'see Chapter 3.4.5.1',
// 'Chapter 3.4.5.1',
// '.1',
// index: 22,
// input: 'For more information, see Chapter 3.4.5.1' ]
// 'see Chapter 3.4.5.1' 是整個匹配。
// 'Chapter 3.4.5.1' 被'(chapter \d+(\.\d)*)'捕獲。
// '.1' 是被'(\.\d)'捕獲的最後一個值。
// 'index' 屬性(22) 是整個匹配從零開始的索引。
// 'input' 屬性是被解析的原始字符串。
15.str.search(regexp)執行正則表達式和String對象之間的一個搜索匹配。如果匹配成功,則 search() 返回正則表達式在字符串中首次匹配項的索引。否則,返回 -1。
下例記錄了一個消息字符串,該字符串的內容取決於匹配是否成功。
function testinput(re, str){
var midstring;
if (str.search(re) != -1){
midstring = " contains ";
} else {
midstring = " does not contain ";
}
console.log (str + midstring + re);
}
16.str1.localeCompare(str2) 比較兩個字符串,返回一個整數,如果小於0,表示第一個字符串小於第二個字符串;如果大於0,表示第一個字符串大於第二個字符串。
'apple'.localeCompare('banana'); //-1
該方法最大的特點就是會考慮語言的順序。看下面的例子:
'B' > 'a' // false 因為Unicode編碼中,大寫字母都在小寫字母前面:B的碼點是66,而a的碼點是97。
'B'.localeCompare('a'); //1 考慮了語言的自然順序
Number
Number.prototype.toFixed() 用於將一個數轉換為指定位數(有效範圍為0到20)的小數,返回這個小數對應的字符串。
var a = 1.234
a.toFixed(2);//1.23
Number.prototype.toExponential() 用於將一個數轉為科學計數法形式。參數為保留的小數位數,有效範圍0-20。
(1234).toExponential() // "1.234e+3"
(1234).toExponential(1) // "1.2e+3"
(1234).toExponential(2) // "1.23e+3"
Number.prototype.toPrecision() 用於將一個數字轉為指定位數的有效數字。參數為有效數字的位數,範圍1-21。
(12.34).toPrecision(1) // "1e+1"
(12.34).toPrecision(2) // "12"
(12.34).toPrecision(4) // "12.34"
(12.34).toPrecision(5) // "12.340"
Boolean
記住:
Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean('') // false
Boolean(NaN) // false
Boolean(1) // true
Boolean('false') // true
Boolean([]) // true
Boolean({}) // true
Boolean(function () {}) // true
Boolean(/foo/) // true
16.Array
構造器對象(自身)屬性、方法:prototype,Array.isArray(obj)
原型對象屬性、方法:constructor,
- 1.改變原數組: splice,push,pop[刪除最後一個],shift[刪除第一個],unshift[添加到第一個],reverse[反轉],sort
- 2.不改變原數組: concat,slice,indexOf,lastIndexOf
- 3.對數組進行遍歷的方法:every,filter,map,forEach,reduce
實例對象屬性、方法:length
length屬性是可寫的,如果設置length到小於數組當前長度,那麼數組長度會縮短。
因此,將數組清空的一個有效方法,就是將數組的length設置為0。舉例如下:
var arr = [1,2,3];
arr.length = 0;
arr; []
當length屬性設為大於數組個數時,讀取新增的位置都會返回undefined。
注意:只要有length的對象就是類數組對象。典型的類似數組的對象是函數的arguments對象,以及大多數DOM元素集,還有字符串。
將類數組對象轉化為對象的方法是:var arr = Array.prototype.slice.call(arrayLike);。
幾個常用對象方法:
- Array.prototype.sort:排序,並返回數組
語法: arr.sort([compareFunction])
如果compareFunction(a, b)返回值小於0,a在b前;返回值大於0,a在b後。
比較函數格式如下:
function compare(a, b) {
if (a is less than b by some ordering criterion) {
return -1;
}
if (a is greater than b by the ordering criterion) {
return 1;
}
// a must be equal to b
return 0;
}
希望比較數字而非字符串,比較函數可以簡單的以 a 減 b,如下的函數將會將數組升序排列
function compareNumbers(a, b) {
return a - b;
}
var fruit = ['cherries', 'apples', 'bananas'];
fruit.sort();
// ['apples', 'bananas', 'cherries']
var scores = [1, 10, 21, 2];
scores.sort();
// [1, 10, 2, 21]
// 注意10在2之前,
// because '10' comes before '2' in Unicode code point order.
var things = ['word', 'Word', '1 Word', '2 Words'];
things.sort();
// ['1 Word', '2 Words', 'Word', 'word']
// 在Unicode中, 數字在大寫字母之前,
// 大寫字母在小寫字母之前.
var numbers = [4, 2, 5, 1, 3];
numbers.sort(function(a, b) {
return a - b;
});
console.log(numbers);
// [1, 2, 3, 4, 5]
2.Array.prototype.splice(start[, deleteCount, item1, item2, ...]):從數組中添加、刪除或替換元素,並返回被刪除的元素列表。會更改原數組。如果deleteCount被省略,則其相當於(arr.length - start)。
var arr = [1,2,3,4];
let deleteArr = arr.splice(1, 2, 'a', 'b');
deleteArr; //[2, 3]
arr; // [1, 'a', 'b', 4]
3.Array.prototype.forEach:遍歷數組元素並調用回調函數
語法:arr.forEach(callback[,thisArg])
function callback(value, index, array){ ... }
var arr = ["a", ,"b"];
arr.forEach(function(value, index, array){ console.log(value);})
//"a"
//"b"
//空元素被跳過去了哦 (,• ₃ •,)
注意:forEach方法無法中斷執行,總會將所有成員遍歷完,如果希望符合某種條件就中斷執行,需要使用 for 循環。
4.arr.filter(callback) 返回一個新數組,包含通過了callback函數中的過濾條件的所有元素
語法:
let newArr = arr.filter(function(element, index, array){ ... })
function isBigEnough(value) {
return value >= 10;
}
var filtered = [12, 5, 8, 130, 44].filter(isBigEnough);
// filtered is [12, 130, 44]
5.arr.map(callback)返回一個新數組,結果是該數組中的每個元素調用提供的callback函數之後的結果
語法:
let newArr = arr.map(function(element, index, array){ ... })
let numbers = [1, 5, 10, 15];
let roots = numbers.map(function(x, index, array) {
return x * 2;
});
// roots is now [2, 10, 20, 30]
// numbers is still [1, 5, 10, 15]
當需要返回值的時候,一般使用map;不需要時,一般使用forEach。
6.arr.every(callback)測試數組的所有元素是否都通過了指定函數的測試,返回一個布爾值。
語法:
let passed = arr.every(function(element, index, array){ ... })
function isBigEnough(element, index, array) {
return (element >= 10);
}
var passed = [12, 5, 8, 130, 44].every(isBigEnough);
// passed is false
passed = [12, 54, 18, 130, 44].every(isBigEnough);
// passed is true
7.arr.concat(arr1[, arr2, ...])合併兩個或多個數組
let arr3 = arr1.concat(arr2);
8.arr.slice(begin, end)返回一個從begin到end(不包括end)這之間的數組淺拷貝到一個新的數組對象。原始數組不會被修改。
var arr = [1,2,3,4];
let newArr = arr.slice(1,3);
arr; //[1,2,3,4];
newArr; //[2,3];
arr.slice(); // [1,2,3,4] 不加參數時返回原數組的拷貝
9.arr.indexOf(searchVal[, fromIndex = 0])返回在數組中可以找到的給定元素的第一個索引,不存在返回-1
10.arr.lastIndexOf(searchVal[, fromIndex = arr.length - 1])返回在數組中可以找到的給定元素的最後一個索引,不存在返回-1
11.arr.reduce() 從左到右依次處理數組的每個成員,最終累計為一個值。
方法的第一個參數是一個函數,這個函數接收4個參數:
1.累積變量:默認數組的第一個變量
2.當前變量:默認數組的第二個變量
3.當前位置(從0開始)
4.原數組
前兩個參數是必須的。
求數組成員之和:
var arr = [1,2,3,4,5];
arr.reduce(function(x, y) {
console.log(x, y);
return x + y;
})
// 1 2
// 3 3
// 6 4
// 10 5
// 15
第一輪執行,x是數組的第一個成員,y是數組的第二個成員。從第二輪開始,x為上一輪的返回值,y為當前數組成員,直到遍歷完所有成員,返回最後一輪計算後的x。
如果要對累積變量指定初值,可以把它放在reduce方法和reduceRight方法的第二個參數。
var arr = [1,2,3,4,5];
arr.reduce(function(x, y) {
console.log(x, y);
return x + y;
}, 10)
// 10 1
// 11 2
// 13 3
// 16 4
// 20 5
// 25
由於 reduce 方法依次處理每個元素,所以它實際上還可以用來搜索某個元素。比如,找出長度最長的數組元素:
function findLongest(entries) {
return entries.reduce(function (longest, entry){
return longest.length > entry.length ? longest : entry;
})
}
findLongest(['aaa', 'bbbb', 'cc'])
17.Function
採用function命令聲明函數時,整個函數會像變量聲明一樣被提升到代碼頭部。比如下面的代碼不會報錯:
f();
function f() { ... }
但是下面的代碼卻會報錯:
f();
var f = function () { ... }
//相當於下面這樣
var f;
f();
f = function(){ ... }
自身屬性,方法:prototype
原型對象屬性,方法:constructor,apply,call,bind
實例對象屬性,方法:name,length,prototype ,arguments,caller
name 返回緊跟在 function 關鍵字之後的函數名。
length 返回函數定義中的參數個數。
toString() 返回函數的源碼。f.toString()
幾個常用對象方法:
1.Function.prototype.apply:通過傳入參數指定 函數調用者 和 函數參數(一個數組或類數組對象) 並執行該函數。
語法:funcObj.apply(thisArg[, argsArray])
thisArg : 函數運行時指定的this值
argsArray : 一個數組或類數組對象
栗子:
(1) Object.prototype.toString.apply("123");// "[object String]"
(2) 使用apply可以允許你在本來需要遍歷數組變量的任務中使用內建的函數,比如獲得一個數組中最大的值:
var arr = [2, 3, 7, 1];
Math.max.apply(null, arr);// 7
//這等價於 Math.max(arr[0], arr[1], arr[2], arr[3])
(3) 還可以用於 使用任意一個對象(可以不是當前自定義對象的實例對象)去調用自定義對象中定義的方法
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.move = function(x1, y1) {
this.x += x1;
this.y += y1;
}
var p = new Point(0,0); //p是一個點
p.move(2,2); // p這個點的位置在x軸右移了2,y軸右移了2
//等價於
p.move.apply(p, [2,2]);
//現在我們有一個圓,原點位於(1,1),半徑r=1
var c = {x: 1, y: 1, r: 1}
//要求x軸右移2,y軸右移1
p.move.apply(c, [2,1]);
2.Function.prototype.bind:通過傳入參數指定 函數調用者 和 函數參數(一個列表) 並返回該函數的引用而並不調用。
語法:funcObj.bind(thisArg[, args])
thisArg : 函數運行時指定的this值
args : 一個列表(不是數組)
栗子:
(1)綁定函數調用的this值,避免錯誤的使用了將this指向全局作用域
this.x = 9;
var module = {
x: 81,
getX: function() { return this.x; }
};
module.getX(); // 返回 81
var retrieveX = module.getX;
retrieveX(); // 返回 9, 在這種情況下,"this"指向全局作用域
// 創建一個新函數,將"this"綁定到module對象
// 新手可能會被全局的x變量和module裏的屬性x所迷惑
var boundGetX = module.getX.bind(module);
boundGetX(); // 返回 81
(2)返回函數的引用而並不調用的另一個好處是:可以指定函數執行的時間。比如有的需求是,在過了一段時間後執行這個函數。
還是上面Point的例子,要求在1000ms以後移動圓:
var c = {x: 1, y: 1, r: 1}
//要求x軸右移2,y軸右移1
var circlemove = p.move.bind(c, 2, 1);
setTimeout(circlemove, 1000);
子類構造器
上面例子中,調用其他對象的方法也可以通過繼承的方法實現:
function Circle(x, y ,r) {
Point.apply(this, [x, y]);
this.radius = r;
}
//將Circle的原型對象指定為Point的實例對象,這樣Circle.prototype上就有了move(x,y)方法
Circle.prototype = Object.create(Point.prototype);
//此時Circle.prototype.constructor屬性還等於function Point(x,y),所以要手動改成function Circle(x,y,r)
Circle.prototype.constructor = Circle;
//還可以定義一些其他的方法
Circle.prototype.area = function () {
return Math.PI*this.radius*this.radius;
}
var c = new Circle(1,2,3);
c.move(2,2);//調用的是自己原型上從Point繼承過來的方法
c.area();
構造器對象Circle的原型鏈:
實例對象c的原型鏈:
function作為普通函數,有3種調用方式:() , apply , call
函數參數的三個特點:
- 形參個數不一定等於實參個數
- 所有參數傳遞都是
值傳遞,也就是説,參數傳遞都只是在棧內存中的操作。所以要特別小心引用類型的參數傳遞,可能會改變傳進來的實參的值。 - 通過
參數類型檢查實現函數重載
18.RegExp、Date、Error
RegExp
構造方法:
- /pattern/flags
- new RegExp(pattarn[, flags]);
原型對象屬性、方法:test, exec
test:使用正則表達式對字符串進行測試,並返回測試結果
語法:regObj.test(str)
var reg = /^abc/i;
reg.test('Abc123'); //true
reg.test('bc123');//false
Date
Date.now() 返回當前距離 1970年1月1日 00:00:00 UTC的毫秒數。
Date.parse() 解析日期字符串,返回距離待解析日期距離 1970年1月1日 00:00:00 UTC的毫秒數。
Date.parse('2017-10-10T14:48:00') // 1507618080000
Date.parse('2017-10-10') // 1507593600000
Date.parse('Aug 9, 1995') // 807897600000
Date.prototype.toString() 返回一個完整的日期字符串。
var d = new Date(2013, 0, 1);
d.toString()
// "Tue Jan 01 2013 00:00:00 GMT+0800 (CST)"
Date.prototype.toLocaleDateString() 返回一個字符串,代表日期的當地寫法。
var d = new Date(2013, 0, 1);
d.toLocaleDateString()
// 中文版瀏覽器為"2013年1月1日"
// 英文版瀏覽器為"1/1/2013"
Date.prototype.toLocaleTimeString() 返回一個字符串,代表時間的當地寫法。
var d = new Date(2013, 0, 1);
d.toLocaleTimeString()
// 中文版瀏覽器為"上午12:00:00"
// 英文版瀏覽器為"12:00:00 AM"
get類方法:
getTime():返回距離1970年1月1日00:00:00的毫秒數,等同於valueOf方法。
getDate():返回實例對象對應每個月的幾號(從1開始)。
getDay():返回星期幾,星期日為0,星期一為1,以此類推。
getYear():返回距離1900的年數。
getFullYear():返回四位的年份。
getMonth():返回月份(0表示1月,11表示12月)。
getHours():返回小時(0-23)。
getMilliseconds():返回毫秒(0-999)。
getMinutes():返回分鐘(0-59)。
getSeconds():返回秒(0-59)。
getTimezoneOffset():返回當前時間與UTC的時區差異,以分鐘表示,返回結果考慮到了夏令時因素。
一個計算本年度還剩多少天的例子:
function leftDays() {
var today = new Date();
var endYear = new Date(today.getFullYear(), 11, 31, 23, 59, 59, 999);
var msPerDay = 24 * 60 * 60 * 1000;
return Math.round((endYear.getTime() - today.getTime()) / msPerDay);
}
var myDate = new Date();
var myDate = new Date(2017,3,1,7,1,1,100);
var year = myDate.getFullYear(); // 四位數字返回年份
vat month = myDate.getMonth(); // 0-11
var date = myDate.getDate(); // 1-31
var day = myDate.getDay(); //0-6
var h = myDate.getHours(); // 0-23
var m = myDate.getMinutes(); // 0-59
var s = myDate.getSeconds(); // 0-59
var ms = myDate.getMilliseconds(); //0-999
var allmMS = myDate.getTime(); //獲取從1970至今的毫秒數 得到的值可以用於new Date(ms);
小練習:獲取昨天的日期:
var today = new Date();
var todayms = today.getTime();
var oneDay = 24*60*60*1000;
var yesterday = new Date(todayms - oneDay);
Error 錯誤處理機制
Javascript的6種原生錯誤類型:
SyntaxError解析代碼時發生的語法錯誤ReferenceError引用不存在的變量時發生的錯誤RangeError當一個值超出有效範圍時發生的錯誤TypeError變量或參數不是預期類型時發生的錯誤。比如new 123URIErrorURI相關函數的參數不正確時拋出的錯誤。主要涉及encodeURI(),decodeURI(),encodeURIComponent(),decodeURIComponent(),escape()和unescape()這六個函數。EvalErroreval函數沒有被正確執行時拋出。該錯誤類型已不再在ES5中出現了。
以上6種派生錯誤,連同原始的Error對象,都是構造函數。
19.標準內置對象中的非構造器對象之——Math,Json
Math對象是擁有一些屬性和對象的單一對象,主要用於數字計算
常用方法:
Math.floor(num) :向下取整
Math.ceil(num)向上取整
Math.random()返回0-1之間的一個偽隨機數,一定小於1,但可能等於0。
用處:
1.任意範圍內生成隨機整數
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min +1 )) + min;
}
2.返回指定長度的隨機字符:
function random_str(length) {
var ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
ALPHABET += 'abcdefghijklmnopqrstuvwxyz';
ALPHABET += '0123456789-_';
var str = '', randIndex = '';
for(var i=0; i < length; i++) {
randIndex = Math.floor(Math.random() * ALPHABET.length);
str += ALPHABET.charAt(randIndex);
}
return str;
}
JSON對象主要用於存儲和傳遞文本信息
常用方法:
JSON.parse(jsonStr):將JSON字符串解析為JSON對象
JSON.stringify(jsonObj):將JSON對象序列化為JSON字符串
20.標準內置對象中的非構造器對象之——全局對象
屬性:NaN,Infinity,undefined
方法:parseInt,parseFloat,isNaN,isFinite,eval
處理URI的方法:encodeURIComponent,decodeURIComponent,encodeURI,decodeURI
構造器屬性:Boolean,String,Number,Object,Function,Array,Date,Error...
對象屬性:Math,JSON
(1)NaN不等於任何值,包括它自己本身。
(2)parseInt(string[, radix]) 用於將字符串轉換為數字,參數:要轉換的字符串,進制(默認十進制)
注意:如果第一個參數不是字符串,會將其先用String(param)轉換為字符串,所以parseInt會將空字符串,true,null轉換為NaN。
parseInt('1'); //1 * 10^0 = 1
parseInt('1', 16); //1 * 16^0 = 1
parseInt('1f'); // 1 * 10^0 = 1
parseInt('1f'); //1 * 16^1 + f * 16^0 = 16 + 15 = 31
parseInt('f1'); // NaN
將非字符串先轉換為字符串還會導致一些意外的結果:
parseInt(0x11, 36) // 43
// 等同於
parseInt(String(0x11), 36)
parseInt('17', 36)
上面代碼中,十六進制的0x11會被先轉為十進制的17,再轉為字符串。然後,再用36進制解讀字符串17,最後返回結果43。
radix取值在2-36之間,0、null、undefined都會被直接忽略,當做默認的10來處理:
parseInt('10', 1); // NaN
parseInt('10', 37); // NaN
parseInt('10', 0); // 10
parseInt('10', null); // 10
parseInt('10', undefined); // 10
parseInt('10', 2); // 2 ( 1*2^1 + 0*2^0 )
(3)parseFloat用於將字符串轉換為浮點數。注意,它與parseInt一樣,都會將空字符串,true,null轉換為NaN:
parseFloat(true) // NaN
Number(true) // 1
parseFloat(null) // NaN
Number(null) // 0
parseFloat('') // NaN
Number('') // 0
parseFloat('123.45#') // 123.45
Number('123.45#') // NaN
(4)eval計算某個字符串,並執行其中的Javascript代碼。
語法:eval(string)
不建議使用,會有性能問題。
(5)encodeURIComponent:用於將URI參數中的中文、特殊字符等作為URI的一部分進行編碼
語法:encodeURIComponent(URIString)
栗子:
1.防止注入性攻擊:
var url = "http://www.xxx.com/index.html?name=" + encodeURIComponent(name);
2.依賴URL進行參數傳遞時,對於參數中有特殊字符(如"&","/")的要對其進行編碼:
var url = "http://www.xxx.com/index.html?name=" + encodeURIComponent("Tom&Jerry") + "&src=" + encodeURIConponent("/assert/image/test.png");
表達式與運算符
介紹表達式、算術運算符、位運算符、布爾運算符、關係運算符、相等與全等、條件運算符。
21.表達式
JS短語,解釋器可以執行它並生成一個值。
22.運算符
+ :加法運算符(既可以處理算術的加法,也可以處理字符串連接)
算法步驟:
- 如果運算子是對象先自動轉成原始類型的值(即,先valueOf(),如果結果還是對象,就toString();Date類型則先toString())
- 兩個運算子都為原始類型之後,如果其中一個為字符串,則兩個都轉為字符串,拼接。
- 否則,兩個運算子都轉為數值,進行加法運算。
[1, 2] + [3]
// "1,23"
// 等同於
String([1, 2]) + String([3])
// '1,2' + '3'
加法運算符以外的其他算術運算符(比如減法、除法和乘法),都不會發生重載。它們的規則是:所有運算子一律轉為數值,再進行相應的數學運算。
1 - '2' // -1
1 * '2' // 2
1 / '2' // 0.5
=== :類型相同且值相等
var a = "123";
var oa = new String("123");
a === oa; // false 其中 a 為string值類型,oa為object對象類型
//undefined 與 null 都與自身嚴格相等
undefined === undefined // true
null === null // true
== :判斷操作符兩邊對象或值是否相等
規則用偽代碼表示為:
function equal(a, b) {
if a、b類型相同
return a === b;
else a、b類型不同
return Number(a) === Number(b);
}
"99" == 99 //true
"abc" == new String("abc") // true
例外:
1.null == undefined //true
2.null與undefined進行 == 運算時不進行類型轉換
0 == null //false
null == false // false
"undefined" == undefined // false
!!x 表示取x表達式運行後的Boolean值
注意&& 和 || 都有短路功能
且運算符的運算規則:如果第一個運算子的布爾值為true,則返回第二個運算子的值(注意不是布爾值);如果第一個運算子的布爾值為false,返回第一個運算子的值(不是布爾值)。或運算符的運算規則:如果第一個運算子的布爾值為
true,則返回第一個運算子的值(注意不是布爾值),且不再對第二個運算子求值;如果第一個運算子的值為false,則返回第二個運算子的值(不是布爾值)。
位運算符
用於直接對二進制位進行計算。
異或運算:^。表示當兩個二進制位不相同,則結果為1,否則結果為0.
常用於在不定義新變量的條件下互換兩個數的值:
var a = 3;
var b = 7;
a^=b;
b^=a;
a^=b;
a; // 7
b; // 3
開關作用
位運算符可以用作設置對象屬性的開關。
假設某個對象有4個開關,每個開關都有一個變量。那麼可以設置一個四位的二進制數,用來表示四個開關
var FLAG_A = 1; // 0001
var FLAG_B = 2; // 0010
var FLAG_C = 4; // 0100
var FLAG_D = 8; // 1000
上面代碼設置A、B、C、D四個開關,每個開關分別佔有一個二進制位。
然後就可以用與運算檢驗當前是否打開了某個開關。
var flags = 5; // 0101
if(flags && FLAG_C) {
console.log('當前打開了C開關.');
}
// 當前打開了C開關.
逗號運算符,
用於對兩個表達式求值,並返回後一個表達式的值。
var x = 0;
var y = (x++, 10);
x; // 1
y; // 10
運算符優先級: * / % 高於 + - 高於 && || 高於 ? :
完整表格:
左結合與右結合
大部分運算符是從左到右計算的,少數是從右向左計算。最常見的是賦值運算符 = 和 三元條件運算符 ? :
var x = y = z = m;
var q = a ? b : c ? d : e ? f : g;
//其實等價於:
var x = (y = (z = m));
var q = a ? b : (c ? d : (e ? f : g));
JS語句
條件控制語句
循環控制語句
for in :遍歷對象的屬性
function Car(id, type, color) {
this.id = id;
this.type = type;
this.color = color;
}
var benz = Car(12345, "benz", "black");
for(var key in benz) {
console.log(key + ": " + benz[key]);
}
//id: 12345
//type: benz
//color:black
//當這個對象有函數時遍歷就會把原型對象上的方法也遍歷出來
Car.prototype.start = function() {
console.log(this.type + "start");
}
//遍歷的時候會多一項
//start: function(){ ... }
//但我們通常是不需要方法的,而方法通常都是存在於原型對象上,所以可以通過hasOwnProperty判斷是否為對象本身屬性:
for(var key in benz) {
if(benz.hasOwnProperty(key)) {
console.log(key + ":" + benz[key]);
}
}
異常處理語句
try catch finally
throw
with語句
通過暫時改變變量的作用域鏈(將with語句中的對象添加到作用域鏈的頭部),來減少代碼量。
(function(){
var a = Math.cos(3 * Math.PI) + Math.sin(Math.LN10);
});
//用with語句
(function(){
with(Math){
var a = cos(3 * PI) + sin(LN10);
}
})
原型鏈如圖:
變量作用域
主要關注兩個方面:
1. 變量的生命週期和作用範圍。
2. 看到一個變量時,要能找到變量定義的位置。
變量作用域分為靜態作用域和動態作用域:
靜態作用域(詞法作用域):在編譯階段就可以決定變量的引用,只跟程序定義的原始位置有關,與代碼執行順序無關。
動態作用域:在運行時才能決定變量的引用。一步一步執行代碼,把執行到的變量和函數定義從下到上依次放到棧裏面,用的時候選離自己最近的一個(比如有兩個x的定義,選離自己最近的一個)。
JS使用靜態作用域。ES5使用詞法環境管理靜態作用域。
詞法環境
是什麼? 是用來描述和管理靜態作用域的一種數據結構。
組成?
- 環境記錄(
record)[形參、變量、函數等] - 對外部詞法環境的引用(
outer)
什麼時候創建?
一段代碼在執行之前,會先初始化創建一個詞法環境。又因為JS沒有塊級作用域,只有全局作用域和函數作用域,所以在JS中只有全局代碼或者函數代碼開始執行前會先初始化詞法環境。
哪些東西會被初始化到詞法作用域中呢?
- 形參(一定要是這個函數顯式定義的形參,而不是真實傳進來的參數,即不是實參)
- 函數定義(除了這個函數的形參和函數體,需要特別注意:在初始化的時候會保存當前的作用域到函數對象中,即
scope:currentEnvironment)這也是形成閉包的必要條件呀!●▽● - 變量定義(var a = xxx;)
詞法環境是怎樣構成的?
知道了詞法環境的構成,就可以分析代碼是如何執行的,知道里面的變量到底引用的是什麼值。
幾個關於詞法環境的問題
1.形參、函數定義或變量定義名稱衝突時,優先級:函數定義 > 形參 > 變量定義
2.arguments這個對象其實也是在環境記錄中的
3.函數表達式是在執行到函數表達式這一句代碼時才創建函數對象,而函數定義是在代碼初始化時就創建了函數對象,保存了當前作用域。
詞法環境可能發生變化的栗子
with
var foo = "abc";
with({ foo: "bar" }) {
function f() { alert foo; }
(function() { alert foo; })(); //這就是一個函數表達式,只有在執行到這一句時才會創建函數對象
f();
}
注意函數定義與函數表達式的區別:
因為詞法環境只會記錄【形參,函數定義,變量定義】這三項。
而且JS中只有全局作用域和函數作用域,沒有塊級作用域,所以所有不在函數作用域中定義的變量或者函數都是屬於全局作用域的。
而with會創建一個臨時的作用域,叫with的詞法環境,在with中用var定義一個變量與在with外一樣,都是在代碼初始化時就被放到詞法環境中,而函數表達式則是在執行到那一句代碼時再放進詞法環境中。
所以函數定義f()的詞法環境outer指向global,而函數表達式詞法環境的outer指向with。
注意:這裏僅僅拿with舉例,我們不建議使用with,容易造成混淆。
try-catch
catch的代碼塊與with的代碼塊很相似,也是臨時創建的詞法環境。
左側的 closure Environment 即為右邊紅框圈出來的函數表達式的詞法環境,outer指向catch Environment。
帶名稱的函數表達式(不常用)
(function A(){
A = 1;
alert(A);
})();
因為A是一個函數表達式,並不是一個函數定義,所以A不會定義到global Environment中。
然後執行到這一句時,與之前的匿名函數表達式類似,創建一個該函數表達式的詞法環境,把A這個函數表達式定義到這個詞法環境中。而且在A的環境裏定義的A函數不能被修改,所以A = 1是沒有用的,仍然會alert出 function A(){A=1;console.log(A);}。
參考資料:wikipedia Scope
函數體內部聲明的函數,作用域綁定函數體內部
function foo() {
var x = 1;
function bar() {
console.log(x);
}
return bar;
}
var x = 2;
var f = foo();
f() // 1
上面代碼中,函數foo內部聲明瞭一個函數bar,bar的作用域綁定foo。當我們在foo外部取出bar執行時,變量x指向的是foo內部的x,而不是foo外部的x。正是這種機制,構成了下文要講解的“閉包”現象。
來自阮一峯的這篇文章
一個困擾我很久的問題:函數定義和函數表達式到底怎樣區分?!
一直就沒搞懂過這個問題,看了半天MDN上的文檔,都是在説:
函數定義與函數表達式基本是一樣滴
因為那些在等號右邊的只能是表達式,所以它們是函數表達式
雖然,函數表達式一般情況下沒有名字,但是呢,它們也是可以有名字滴,有名字之後就與函數聲明的語法真的一樣了,這還怎麼區分啊摔!(´Д`)
不信你自己看 MDN函數那一章 看的我都要哭了
直到我去翻了阮大大博客裏講函數的一篇, ⊙▽⊙ 裏面講立即調用的函數表達式(IIFE)一節裏有這樣一句話:
為了避免解析上的歧義,JavaScript引擎規定,如果function關鍵字出現在行首,一律解釋成語句。因此,JavaScript引擎看到行首是function關鍵字之後,認為這一段都是函數的定義。
所以這樣句首不是function的(function(){})();就是函數表達式啦!下面這樣也可以:
var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();
// 甚至這樣也可以:
!function(){ /* code */ }();
~function(){ /* code */ }();
-function(){ /* code */ }();
+function(){ /* code */ }();
閉包
什麼是閉包?
閉包是由函數和它引用的外部詞法環境組合而成。
閉包允許函數訪問其引用的外部詞法環境中的變量(又稱自由變量)。
看個栗子:下面的函數add()最終return的值是一個函數,且在add函數外調用return出來的這個匿名函數時可以引用add函數內部的環境,使用add函數內部定義的變量i。
function add(){
var i = 0;
return function() {
console.log(i++);
}
}
var f = add();
f(); // 0
f(); // 1
一般來説,一個函數執行完之後,內部的局部變量都會被釋放掉,就不存在了。但是JS比較特殊,因為JS中允許函數作為返回值,當函數作為返回值,這個函數就保存了對外部詞法環境的引用,而相應的,外部詞法環境中定義的變量就不會被釋放掉,會一直保留在內存中。這就是JS的閉包。閉包使得它引用的函數的內部環境一直存在,所以閉包可以看作是它引用的函數的內部環境的一個接口。
廣義上來説,所有的JS函數都可以稱為閉包,因為所有的JS函數創建後都保存了當前的詞法環境。
閉包的應用
閉包最大的作用有兩個,一個是可以讀取函數內部的變量,另一個是使這些變量始終保存在內存中。
1.使外部詞法環境的變量始終保存在內存中
function createIncrementor(start) {
return function () {
return start++;
};
}
var inc = createIncrementor(5);
inc() // 5
inc() // 6
inc() // 7
start 存在於 createIncrementor 的詞法環境中,通過閉包inc被保留在內存中,因此可以累加。
2.保存變量現場
需求:為一組元素註冊onclick事件,當點擊其中某一個元素觸發onclick事件時能將該元素的索引打印出來。
看下面這段代碼是否可以實現?
var addHandlers = function(nodes) {
for(var i=0; i<nodes.length; i++){
nodes[i].onclick = function(){
alert(i);
}
}
}
jsfiddle自己試試
試過會發現:每次彈出的都是3,所以這樣寫是不能實現的。因為JS沒有塊級作用域,所以每次for循環不會形成單獨的作用域,因此最內層的匿名function函數中所有的i共享它的外部詞法環境addHandler函數中的同一個變量i。而當onclick被調用時,循環早已結束,所以i的值也就是循環結束後的nodes.length。
想要更清晰一些,可以把for循環拆開看看:
var addHandler = function(nodes){
var i = 0;
nodes[i].onclick = function(){
alert(i);
}
//等價於
//nodes[0].onclick = function(){
// alert(i); 注意這裏還是i,當onclick被調用,循環已經結束了,也就是addHandler函數執行到最後了,所以i是3
//}
i = 1;
nodes[i].onclick = function(){
alert(i);
}
i = 2;
nodes[i].onclick = function(){
alert(i);
}
i = 3;
}
addHandler(document.getElementsByTagName('a'));
這樣總可以理解了吧。
如何改進?
既然問題出在每一次for循環沒有能夠形成單獨的作用域來保存每一次for循環的變量i的值,那麼要解決的就是這個問題。
方法就是用剛剛學的閉包:在內層匿名函數外面與for循環之間加一個幫助函數helper,helper有一個形參i,在每一次for循環時,都調用一次helper(i),傳入本次循環的i,helper接收了這個形參之後,把它存到自己的詞法環境中,內部的匿名function函數就可以訪問這個形參,再將這個匿名函數return出去,這就形成了一個閉包。這樣就做到了每次循環都形成一個閉包。因此就可以把傳入的i的值給保存下來。將來當onclick事件被觸發時,就會調用helper函數return出來的匿名函數,也就可以使用helper函數的內部詞法環境,彈出當時存進去的那個i的值。
劃重點:
每一次for循環,調用一次helper(i)函數;
每一次onclick事件被觸發,調用helper內部return出來的匿名函數,這個匿名函數的outer指向helper函數內部。
var addHandlers = function(nodes) {
var helper = function(i) {
return function(){
alert(i);
}
}
for(var i=0; i<nodes.length; i++){
nodes[i].onclick = heapler(i);
}
}
jsfiddle裏置幾試試
如果還是無法理解,那麼我們再來把for循環拆開看看
var addHandler = function(nodes){
var helper = function(i){
return function(){
alert(i);
}
}
var i = 0;
nodes[i].onclick = helper(i); // 等價於 nodes[0].onclick = helper(0);
i = 1;
nodes[i].onclick = helper(i);// 等價於 nodes[1].onclick = helper(1);
i = 2;
nodes[i].onclick = helper(i);
i = 3;
}
addHandler(document.getElementsByTagName('a'));
那麼,你能不能再想出一種使用閉包解決這個問題的寫法呢?想想看 (。・ω・)ノ゙
參考寫法:
var addHandler = function(nodes){
var helper2 = function(i) {
nodes[i].onclick = function(){
alert(i);
}
}
for(var i=0; i<nodes.length; i++){
helper2(i);
}
}
addHandler(document.getElementsByTagName('a'));
注意,外層函數每次運行,都會生成一個新的閉包,而這個閉包又會保留外層函數的內部變量,所以內存消耗很大。因此不能濫用閉包,否則會造成網頁的性能問題。
3.封裝
共享函數,私有變量。
var observer = (function(){
var observerList = [];
return {
add: function(obj){
observerList.push(obj);
},
empty: function(obj){
observerList = [];
},
get: function(obj){
return observerList;
}
}
})
可以參考阮大大講的閉包
面向對象
//Teacher constructor
function Teacher() {
this.courses = [];
this.job = 'teacher';
this.setName = function(name){
this.name = name;
}
this.addCourse = function(course){
this.courses.push(course);
}
}
var bill = new Teacher();
bill.setName("Bill");
bill.addCourse("math");
var tom = new Teacher();
tom.setName("Tom");
tom.addCourse("physics");
這樣完全使用構造函數來構造實例對象,他們的存儲是這樣的:
這樣一來,每個實例對象都會存儲一遍所有的屬性和方法,對於所有的實例對象來説,都耗用了一遍內存,這是很浪費的。實際上我們可以看到屬性job和方法setName,addCourse其實是所有實例對象可以共用的,那麼如何實現呢?這就引出了原型。
原型
原型是構造器對象的一個屬性,叫prototype,JS中每個構造器對象都有這樣一個屬性。這個屬性的值就是實例對象的原型,用於實現原型繼承,讓同一個構造器創建出的多個實例對象共享同一個原型,這些實例對象就有了共同的屬性和方法。
用原型改寫上面的Teacher構造器:
//Teacher prototype
function Teacher(){
this.courses = [];
}
Teacher.prototype = {
job: 'teacher',
setName: function(name) {
this.name = name;
},
addCourses: function(course){
this.courses = course;
}
}
var bill = new Teacher();
bill.setName("Bill");
bill.addCourse("math");
var tom = new Teacher();
tom.setName("Tom");
tom.addCourse("physics");
原型鏈如圖所示:
可以看到實例對象bill與tom都有一個隱式的指針指向了Teacher.prototype
原型鏈
上面的原型鏈並不是完整的,我們來看完整的原型鏈的構成:
我們知道,在JS中,函數對象是用於創建自定義構造器對象的構造器對象,所以所有的自定義構造器對象都是Function的一個實例對象;又因為Function也有一個prototype屬性,所以我們自定義的Teacher也有一個隱式的__proto__屬性指向Function.prototype,而這其中有一些引擎自身實現了的一些屬性和方法,比如toString() , apply()等方法。
而又因為Teacher.prototype的對象可以由new Object()來創建,它是Object對象的一個實例,所以Teacher.prototype有一個隱式的__proto__屬性指向Object.prototype。
它們之間其實構成了一個鏈式的關係:tom是以Teacher.prototype為原型的,Teacher.prototype又是以Object.prototype為原型的。這樣就形成了一個原型鏈。
JS中的屬性查找、屬性修改和屬性刪除都是通過原型鏈實現的。
屬性查找
先在對象本身找,找不到就到對象的原型上找。
屬性修改
以tom對象為例:
tom.name = 'Tom Green';
因為tom對象本身有name這個屬性,所以直接修改。
tom.job = 'assitant';
tom對象本身沒有job這個屬性,那麼就為tom對象創建一個job屬性,同時把值賦給這個屬性。注意這裏即使tom對象的原型上有job這個屬性,也不會直接修改原型上的屬性。
如果想要修改原型對象上的屬性,那麼這樣做:
Teacher.prototype.job = "assistant";
屬性刪除
與修改屬性類似,delete也只能刪除對象自身的屬性,不能刪除原型上的屬性。eg:
delete tom.job;
如何判斷屬性是否來自對象本身:
tom.hasOwnProperty('job');
來源於對象本身就返回 true,來源於原型或不存在則返回 false。
使用ES5中的原型繼承構造一個有原型的實例對象: Object.create(proto[, propertiesObject])(而不是使用構造器constructor方法)
可以直接從一個原型對象創建一個新的對象,同時為這個對象指定一個原型。而不需要通過 new Teacher();來完成。
var teacher = {
job: "teacher",
courses: [],
setName: function(name) {
this.name = name;
},
addCourses: function(course) {
this.courses.push(course);
},
}
var bill = Object.create(teacher);
tom.setName('Bill')
這樣我們就不用通過構造器,直接創建一個以teacher對象為原型的bill實例對象。並沒有使用前面説的constructor,prototype等方式。這是ES5中提供的一種新的原型繼承的方式。原型鏈:
JS面向對象
全局變量很容易衝突,所以需要做信息隱藏。最好的方式就是封裝:將一部分信息開放出來,另外一部分隱藏起來。
封裝
Java中有private, protected, public關鍵字來定義屬性和方法的訪問權限。但JS中並沒有,那麼JS中是如何實現封裝的呢?
按照我們原來的寫法,像下圖這樣是沒有實現封裝的:
通過JS語言的特性做類似的模擬。像下圖這樣是可以實現封裝的:
模擬private:在function A(){}內部通過var 定義了一個屬性_config,在外部無法被直接訪問和修改,然後提供了一個函數this.getConfig()來讓我們可以在外部獲取這個屬性。這樣一來就模擬了private的特性。
模擬protected:對於protected和public,在JS中無法做到本質上的區分。可以使用一些人為約束的規則來區分,比如:使用下劃線開頭來定義protected的屬性和方法。這樣雖然實際上是都可以調用的,但是在代碼的書寫上是可以區分出來的。
繼承
類繼承
(function(){
function ClassA(){}
ClassA.classMethod = function(){}
ClassA.prototype.api = function(){}
function ClassB(){
ClassA.apply(this, arguments);
}
//給ClassB的原型賦值了一個ClassA類型的實例對象
ClassB.prototype = new ClassA();
//上述這樣會導致ClassB.prototype的constructor也變成ClassA所以我們要手動改成ClassB
ClassB.prototype.constructor = ClassB;
ClassB.prototype.api = function() {
ClassA.prototype.api.apply(this, arguments);
}
var b = new ClassB();
b.api();
})();
原型鏈:
原型繼承
類繼承可以説是對其他語言的類繼承的一種模擬,而原型繼承就是JS固有的特性了。
(function(){
//先定義一個原型
var proto = {
action1: function(){
},
action2: function(){
}
}
//使用Object.create基於已有對象創建一個新的對象
var obj = Object.create(proto);
})();
編程風格
- 縮進:2個空格。
- 循環和判斷的代碼體總是使用大括號。
- 圓括號。表示函數調用時,函數名與左括號間不加空格;表示函數定義時,函數名與與左括號間不加空格;其他情況,前面位置的語法元素與左括號之間,都加空格。
- 行尾的分號建議不要省略。
- 全局變量儘量減少使用。不得不用時建議用大寫字母。
- 變量聲明。因為JS有變量聲明提升,所以儘量將所有變量聲明在代碼塊頭部。
new命令。用new命令從構造函數生成一個新對象時,如果忘了加上new,新對象內部this關鍵字就會指向全局對象,導致所有綁定在this上的變量都變成全局變量。因此,建議使用Object.create()命令替代new命令。- with語句儘量不要使用。
- 不要使用相等
==運算符,只使用嚴格相等===運算符。 - 語句的合併。建議不要將不同目的的語句合併到同一行。
- 儘量用
+=代替++。比如:用x += 1;代替++x;。 switch ... case每一個case後都要加break語句,這樣造成代碼冗長。而且,使得代碼結構混亂。建議避免使用,改寫成對象結構。
function doAction(action) {
var actions = {
'hack': function() {
return 'hack';
},
'slash': function() {
return 'slash';
},
...
}
}