記錄自己JavaScript基礎概念完整學習的過程
本文是在學習廖雪峯老師的JavaScript教程時 所歸納的筆記
注意點:
- 以 file:// 開頭的地址無法執行聯網的JavaScript
- 雖然js不要強制在語句結尾加; 但是在某些情況下 讓js引擎自動補全 ; 會改變程序語義
- js本身不限制嵌套層級,但是一般不建議嵌套太多,可以把深層代碼抽出作爲函數調用
- 註釋: // 是行註釋 /* */ 是塊註釋
- js嚴格區分大小寫
數據類型和變量
當下問數據類型就要考慮es6了, es6之前的是 null undefined number Boolean object string, es6的是symbol
number
不分整數和浮點數 統一爲Number
number可以直接做四則運算 + - * / ; % 是求餘運算
比較數據時需要注意的地方:
1. == 如果是不相同數據類型做比較,它會自動轉換數據類型再比較(隱式轉換)
2. === 不會轉換比較對象, 它比較會判斷類型 不同類型的數據 無論值如何 一律返回false 相同的話纔會根據值返回
3. NaN與所有制都不相等 包括自己
NaN === NaN // false
唯一能判斷NaN的方法是 isNaN() 函數
4. 浮點數不要直接比較 最好是轉化爲整數再比,因爲js計算浮點數存在精度丟失問題
null和undefined
一個是 空 一個是 未定義
JavaScript的設計者希望用null
表示一個空的值,而undefined
表示值未定義。事實證明,這並沒有什麼卵用,區分兩者的意 義不大。大多數情況下,我們都應該用null
。undefined
僅僅在判斷函數參數是否傳遞的情況下有用
object
普通對象的鍵值都是字符串類型, es6 中的Map對象的鍵值可以是任何類型
變量
這裏只說三個注意點:
1. 變量命名是大小寫英文 數字 $和_ 這四類的組合,且首位不能爲數字
2. 變量名不能是javascript關鍵字
3. 變量生命符現在有 let var const 三種,可點進去查看詳細解釋
string 字符串
基礎字符串不多做贅述。
轉義字符串:
比如 一個字符串中即包含' 也包含" // i'm "ok"
想要輸出這種,可以用轉義字符串改寫爲 // 'i\'m \"ok \" ' 即可
即 轉義字符 \ 要寫在被轉義字符的前面
常用的轉義字符有: \n 換行 \t 製表符 \本身也要轉義 如果要輸出\ 寫法就是 \\
涉及到多行字符串,如果想要其效果輸出爲多行 即有換行效果
則\n 就比較費事了, so es6新增了標準多行字符串的表示方法
即 ` ... ` 反引號包裹
` 這是一首
簡單的
小情歌
唱着我們心頭的
曲折 `
模板字符串
對於字符串及變量拼接操作,
常規的是這樣的
var a = 'hello'; var b = a + 'world'; // b hello world
如果有很多變量需要連接,+ 就會比較麻煩
es6中新增了一種 模板字符串
表示方法如下
var a = 'hello'; var b = `${a} world`; b // hello world
用反引號包裹 反引號中${}內的部分既是對於變量的值
對字符串的操作:
var str = 'hey bro';
常見的有:
str.length // 取字符串長度
str[0] // h 取指定位置的值 取法類似數組那樣 通過對應索引取值
tips:
字符串是不可變的,如果根據某個索引對字符串指定位置賦值,不會報錯,但是也不會有任何效果
var s = 'nb'; s[0] = 's'; console.log(s) // nb
js爲操作字符串提供的一系列方法:
這些方法不會改變原有字符串內容,統一返回一個新的字符串。
1. toUpperCase() 把字符串全部變爲大寫
2. toLowerCase() 把字符串全部變爲小寫
3. indexOf() 會搜索指定字符串出現的位置 並返回該位置的下標值 若找不到 返回-1
4. substring() 返回指定索引區間的子串
let s = 'hey bro'; s.substring(0,5); // hey br s.substring(1); // ey bro
數組 array
嚴格來說,array是object的特殊類型
對array的一系列操作方法如下:
1. arr.length // 獲取array的長度
tips: 如果直接給arr的length賦一個新值,會導致array的大小發生變化
var arr = [1,2,3]; arr // length 3 arr.length = 6; arr // [1,2,3,undefined,undefined,undefined] arr.length = 2; arr // [1,2]
2. arr可以通過索引改變對應值,該修改會直接改變arr本身
tips: 通過索引賦值時,如果索引值超過了arr原有範圍,同樣會引起arr大小變化
3. indexOf() 和string的indexOf()用法 返回均相同
4. slice() 截取arr的部分元素, 返回一個新的數組 // 不會對arr產生變化
tips: slice()的起止參數爲開始索引,結束索引, 返回的新數組包括開始索引,不包括結束索引
如果不給任何參數,會從頭到尾截取,相當於複製一個新的數組
5. push() 向arr尾部添加若干元素
6. pop() 把最後一個元素刪掉
7. unshift() 向頭部添加若干元素
8. shift() 將頭部第一個元素刪掉
9. sort() 將arr排序, 自定義順序會在後面講到。 // 改變原數組
10. reverse() 將arr順序完全顛倒 即反轉 // 改變原數組
11. splice() 修改arr指定區域的元素 包括直接刪除 替換
tips: splice() 返回的值是指定的索引對應的元素區域 是一個新數組
原數組會依據調用方式發生對應變化
12. concat() 拼接數組 // 返回新數組
tips: concat()可以接受任意個元素和數組,並最終拼接成一個新數組
13. join() 數組轉字符串 會依據join(x)中添加的x拼接起來
tips: 如果array的元素不是字符串, 將自動轉換爲字符串後再連接
14.
對象 object
檢測對象 object 是否具有某一項屬性,可以用in操作符:
let obj = { name: '97', weight: 84, age: 24 }; 'name' in obj; // true 'sex' in obj; // false
tips:
用 in 來判斷屬性是否存在,有一個坑, 因爲這個屬性不一定是obj的,可能是obj繼承得到的
比如
'toString' in obj; // true
因爲toString是定義在object中的,而所有對象原型鏈的頂部都是object,則obj也擁有toString屬性
爲了規避in的這種情況,可以使用hasOwnProperty()方法
該方法判斷 obj自身是否擁有某屬性
obj.hasOwnProperty('name'); // true obj.hasOwnProperty('toString'); // false
條件判斷
基本的判斷類型不再贅述
下面說一些記錄點:
1. js裏 在做數據類型轉換時, 只把 null undefined 0 NaN和'' 視爲false,其它一概爲true // if(ah)
2. for()循環的三個條件均可省略, 即for(;;), 如果沒有退出循環的條件,必須用break跳出,否則就是死循環
3. for...in 對對象進行操作時 建議使用hasOwnProperty()過濾掉對象繼承的屬性
var o = { name: 'Jack', age: 20, city: 'Beijing' }; for (var key in o) { if (o.hasOwnProperty(key)) { console.log(key); // 'name', 'age', 'city' } }
4. for...in 對array循環時 得到的索引不是number,而是string
Map和Set
因爲基本對象object的鍵值限制爲String類型,爲了可以用其它類型做鍵值
es6引入了新的數據規範Map
Map
map是一組鍵值對的結構,具有極快的查找速度
擬一個情況,要根據同學的名字查找對應的成績,
如果用array實現,則需要一個數組記錄名稱,一個數組記錄成績
var names = ['Michael', 'Bob', 'Tracy']; var scores = [95, 75, 85];
來自阮一峯:Map和Set講義
感覺不能拿這種結構來舉例子,因爲它們是沒有強映射關係的,應該用常規對象 如下
let obj = {
{ name: 'a', score: 75 },
{ name: 'b', score: 66 },
{ name: 'c', score: 90 },
};
隨便給定一個值 66 想要拿到name的值 一定會用循環, 而且 數據體積越大,查找越慢,耗時越長
如果用Map實現,只需要構造成key-value結構,無論數據體積有多大,查找速度都不會慢。
var m = new Map([['a', 75],['b', 66],['c', 90]]); m.get('a'); // 直接輸出75
tips:
初始化Map需要一個二維數組,或者直接初始化空Map,再對其操作。 Map自帶的操作方法如下:
var m = new Map(); // 空map m.set('a', 66); // 添加key-value值操作 m.set('b', 55); // m.has('b'); // 是否存在key b: true m.get('b'); // 55 m.delete('b'); // 刪除key b m.get('b'); // undefined 由於key不能重複,故重複對相同的key設值,後面的值會覆蓋前面的值
Set
set只是key的集合,並非key-value這種形式
初始化Set需要一個數組,或者空
Set中不會包含重複值,有自動過濾去重操作
Set有兩個方法:
add(key) 添加key值 重複添加不會報錯 但是無效
delete(key) 刪除指定key
iterable
對常規array遍歷可以用下標循環,原來的map和set無法使用下標。 爲了統一 集合類型,就有了iterable類型
Array Map Set 都屬於iterable類型
具有iterable類型的集合 可以通過 for...of來遍歷 // 屬於es6新特性
var a = ['A', 'B', 'C']; var s = new Set(['A', 'B', 'C']); var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]); for (var x of a) { // 遍歷Array console.log(x); } for (var x of s) { // 遍歷Set console.log(x); } for (var x of m) { // 遍歷Map console.log(x[0] + '=' + x[1]); }
for...in 和 for...of 的區別
for...in 遍歷的實際上是對象的屬性名, array實際上也是對象,so它的每個元素的索引被當做一個屬性。
手動給array對象添加屬性後, for ... in 的效果如下:
var a = ['A', 'B', 'C']; a.name = 'Hello'; for (var i in a) { consolg.log(i); // '0' '1' '2' 'name' } for...in把name也包裹進去了, length沒有 for ... of 只循環集合本身的元素: var a = ['A', 'B', 'C']; a.name = 'Hello'; for (var x of a) { console.log(x); // 'A', 'B', 'C' }
更好的方案是使用iterable內置的forEach()方法,它接收一個函數,每次迭代就自動回調該函數。 以Array爲例
var a = ['A', 'B', 'C']; a.forEach(function(ele,index,array) { // ele: 指向當前元素 // index: 指向當前索引 // array: 指向array對象本身 即a })
tips: Set的forEach 因爲沒有索引 前兩個參數都是元素本身
Map的forEach 依次爲 value key Map本身
函數
arguments
只在函數內部起作用,並且永遠指向當前函數調用者傳入的所有參數,可以簡單的理解爲,傳入函數的所有參數的參數集合
tips: 即使函數未定義形參,只要有參數傳入,通過arguments就可拿到傳入的所有值
function abs() { if(arguments.length === 0) { return 0 } let x = arguments[0]; return x >= 0 ? x : -x; } abs(); // 0 abs(10); // 10 abs(-9); // 9
arguments最常用於判斷傳入參數的個數。
// foo(a[, b], c) // 接收2~3個參數,b是可選參數,如果只傳2個參數,b默認爲null: function foo(a, b, c) { if (arguments.length === 2) { // 實際拿到的參數是a和b,c爲undefined c = b; // 把b賦給c b = null; // b變爲默認值 } // ... }
爲了規範化不設定參數數量的函數
es6標準引入了rest參數
即
function foo(a,b, ...rest) { console.log('a', a); console.log('b', b); console.log('rest', rest); }
如果傳入的參數連正常定義的參數都沒填滿,也不要緊,rest參數會接收一個空數組(注意不是
undefined
)
變量作用域與結構賦值
變量作用域
JavaScript的函數在查找變量時從自身函數定義開始,從“內”向“外”查找。如果內部函數定義了與外部函數重名的變量,則內部函數的變量將“屏蔽”外部函數的變量
變量提升
js的函數定義 有一個特點, 它會先掃描整個函數體的語句, 把所有申明的變量提升到函數頂部
'use strict': function foo() { var x = 'Hello' + y; // line 1 console.log(x); // line 2 var y = 'Bob'; // line 3 } foo(); 雖然是strict模式,但是 line 1並未報錯 原因就在於變量y在隨後聲明瞭,但是line 2打印的y值依然是undefined
這是因爲,
雖然js引擎會自動提升變量聲明,但是不會提升變量賦值,即賦值操作還是在line 3進行的
javascript只有一個全局作用域。任何變量(包括函數),如果沒有在當前函數作用域下找到,就會繼續往上查找,最後如果在全局作用域中也沒有找到,就會報referenceError錯誤
名字空間
全局變量會綁定到window上,不同的js文件如果定義了相同名字的頂層函數,或者使用了相同的全局變量,都會造成 命名衝突
減少衝突的一個方法是, 把某一個js文件下的所有變量和函數全部綁定到一個全局變量中:
// 唯一的全局變量myApp var myApp = {}; // 其他變量 myApp.other = 'aabb'; // 其他方法 myApp.foo = function () { return 'foo'; }
把自己的代碼全部放入唯一的名字空間中,可大大減少全局變量衝突的可能。
像 jq yui underscore 等js庫都是這麼做的
局部作用域
爲了解決js變量沒有局部作用域的問題,es6引入了新的關鍵字let let 可代替var申明一個塊級作用域變量:
'use strict'; function foo() { for (var i=0; i<100; i++) { // } i += 100; // 仍然可以引用變量i } 'use strict'; function foo() { var sum = 0; for (let i=0; i<100; i++) { sum += i; } i += 1; // SyntaxError: }
常量
es6之前 申明常量通常使用大寫的變量來表示: ‘這是一個常量,不要修改它的值’;
es6 引入了const來定義常量
const 與 let 都具有塊級作用域
const a = 123; a = 44; // 值爲基本數據類型的常量是不可修改其值的 大部分瀏覽器會報錯 少部分不報錯 但是不會有效果 a // 123 const a = [1,2,3]; a[0] = 'c'; a // ['c',2,3]
解構賦值
傳統賦值做法:
var array = ['hello', 'JavaScript', 'ES6']; var x = array[0]; var y = array[1]; var z = array[2];
es6之後 有了解構賦值,現在我們這麼做
var [x, y, z] = ['hello', 'JavaScript', 'ES6'];
解構賦值的要點之一就是 嵌套層次和位置要保持一致
let [x, [y, z]] = ['hello', ['JavaScript', 'ES6']]; x; // 'hello' y; // 'JavaScript' z; // 'ES6'
也對對象進行解構賦值
let obj = { name: '', sex: '', age: '', address: { city: '', province: '' } } let {name,sex,age} = obj; // 寫法一 對象的解構取值 如果取的值不存在 則會賦給其undefined let {id} = obj; // undefined let {address:{city, province}} = obj; // 對象的解構同樣可以多層 只要嵌套解構對應即可 解構中的先後位置無硬性要求 let {school = false, sex} = obj; // 解決取值不存在的undefined問題 賦默認值
有些時候,如果變量已經被聲明,再次賦值的時候,正確的寫法也會報錯
// var x,y; {x, y} = {name: 'sb', x: 100, y: 200} // 語法錯誤: Uncaught SyntaxError: Unexpected token = // 這是因爲JavaScript引擎把{開頭的語句當作了塊處理,於是=不再合法。解決方法是用小括號括起來: ({x, y} = { name: '小明', x: 100, y: 200});
使用場景:
1. 交換變量值
var x=1, y=2; [x, y] = [y, x]
2. 快速獲取需要的對象中屬性
var {hostname:domain, pathname:path} = location;
方法(this)
在一個對象中綁定函數,稱爲這個對象的方法。
var xiaoming = { name: '小明', birth: 1990, age: function () { var y = new Date().getFullYear(); return y - this.birth; } }; xiaoming.age; // function xiaoming.age() xiaoming.age(); // 今年調用是25,明年調用就變成26了
綁定到對象上的函數和普通函數沒什麼區別,但是在它內部用了一個this,這個this就很可以拿來說說了
在一個方法內部, this是一個特殊變量,它始終指向當前對象,so
this.birth
可以拿到xiaoming
的birth
屬性再拆開寫
function getAge() { var y = new Date().getFullYear(); return y - this.birth; } var xiaoming = { name: '小明', birth: 1990, age: getAge }; xiaoming.age(); // 25, 正常結果 getAge(); // NaN
爲什麼getAge() 返回的是NaN呢?
這裏就牽涉到this指向的問題了,
一般情況下,this指向最後調用它的對象,見上圖代碼,xiaoming.age(); this指向的就是對象 xiaoming, 而xiaoming是有age屬性的, 所以age() 方法中的 this.birth就能取到1990
而 getAge(); 因爲是直接在頁面對象下定義的方法, 它的this指向的是頁面對象,也就是全局對象window, 而全局對象裏並未定義birth,所以就返回NaN了
是不是很神奇,更神奇的在下面
var fn = xiaoming.age; // 先拿到xiaoming的age函數 fn(); // NaN
同樣是NaN
所以 想要保證this指向中具有birth 必須用obj.age(); 即xiaoming.age();才能取到
一般情況下,如果希望this總是指向某一個固定對象,可以定義全局變量,或者外部變量,比如 var _that; _that = this;
捕獲該對象的this,在下方使用_that就沒有問題了
修復this指向,還可以用對象原型的自有方法 call() apply() bind()
apply()
function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}
var xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};
xiaoming.age(); // 25
getAge.apply(xiaoming, []); // 25, this指向xiaoming, 參數爲空
call()
function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}
var xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};
xiaoming.age(); // 25
getAge.call(xiaoming, ''); // 25, this指向xiaoming, 參數爲空
call()和apply()的第一位參數都是修改this指向爲傳入對象
唯一區別就是 從第二個參數開始
call()可以有很多參數
apply()只有一個參數 且參數類型需爲數組
對於普通函數調用,我們通常把this綁定爲null
Math.max.apply(null, [3, 5, 4]); // 5 Math.max.call(null, 3, 5, 4); // 5
以上Math也可以看出apply()和call()的區別
裝飾器
因爲函數都是對象的屬性,哪怕是頁面默認函數,也可以改變其行爲。
比如parseInt
現在假定我們想統計一下代碼一共調用了多少次
parseInt()
,可以把所有的調用都找出來,然後手動加上count += 1
,不過這樣做太傻了。最佳方案是用我們自己的函數替換掉默認的parseInt()
'use strict': var count = 0; var oldParseInt = parseInt; window.parseInt = function() { count += 1; return oldParseInt.apply(null,arguments); // 調用原函數 傳入所有參數集arguments }
簡單理解就是 將parseInt 拷貝給oldParseInt, 然後重新定義parseInt操作, 賦予 count統計 再把原來的方法給繼承進去
高階函數
簡單描述,就是接受參數爲函數的函數,就是高階函數
map()
map() 遍歷數組的每一項 同時接收一個函數作爲處理方法,處理這遍歷中的每一項
let arr = [1,2,3,4,5,6]; pow(x) { return x*x } let newarr = arr.map(pow); // newarr [1,4,9,14,25,36]
用一般的for循環同樣可實現該效果
pow (x) { return x*x } let arr = [1,2,3,4,5]; let results = []; for(var i in arr) { results.push(pow(arr[i])) }
那爲什麼要用高階函數map()咧?
廖雪峯老師給出了這樣的解釋:
從上面的循環代碼,我們無法一眼看明白"把f(x)作用在Array的每一個元素並把結果返回,生成一個新的Array".
所以原因簡化爲下面這句話,
高階函數把運算規則抽象化,使我們能夠更清晰的看到發生了什麼,和爲什麼會這麼做。
因爲運算規則被抽象化,還可以計算任意複雜的函數
比如,把Array內的所有數字轉爲字符串
let arr = [1,2,3,4,5,6]; let results = arr.map(String); // 因爲運算規則被抽象化, 所以 輕易不要去研究這些運算過程, 在某一階段前 沒有太大的必要性
reduce()
array的歸併操作,什麼是歸併呢? 我們先看map(), map起到的是遍歷作用,遍歷每一項,這裏的每一項就是每一項,自身獨立,和其他項沒有關係,而歸併,則是從數組的第一位元素開始,一直執行到最後一位,在每次執行開始,會接收對上一位元素執行的結果,舉個簡單的例子,求和,用reduce()求和:
let arr = [1,3,4,5,6]; let mix = arr.reduce(function(x,y) { return x + y }) mix // 25
定義,reduce()同樣接收一個函數對歸併項處理,但是這個函數有要求,就是必須接收兩個參數,reduce()把結果繼續和序列的下一個元素做累積計算,所以,可以簡單地理解爲,參數x,是當前項之前所有項的結果集合,y是歸併的當前項
廖老師的課後練習題:
修正後的代碼是:
function str2int(x) { return parseInt(x) } r = arr.map(str2int);
可正常輸出1,2,3
但是原題的異常原因 , 我沒有找到(還是不想讀英文文檔hhh)
filter()
filter()的作用是過濾Array的某些元素 然後返回剩下的元素
它也接收一個函數,過濾條件就是函數內容
filter()把傳入的函數依次作用於每個元素,然後根據返回值是true還是false決定保留還是丟棄該元素。
tips: 至於它到底有沒有改變原數組,這是個令人疑惑的問題
sort()
排序在各個語言中都是極爲常見的算法,很多語言都內置有排序函數sort()
js的 sort() 排序
實現核心是,
Array的sort()方法 默認把所有元素先轉換爲String再排序, 而對String的排序又會轉換成對其對應位的ASCII碼比較
直接arr.sort() 它就是這麼幹的
如果不想遵循它既定的規則,那麼我們可以傳入一個排序函數,來實現我們想要的效果
比如 按數字大小排序:
let arr = [10,20,1,2]; arr.sort(function(x, y) { if (x < y) { return -1 } if (x > y) { return 1 } return 0; }) console.log(arr); // [1,2,10,20] 至於 return -1 return 1 return 0 分別代表什麼 和爲什麼這麼return可以那麼代表,請自行摸索
Array() 對象自身的高階函數
every() find() findIndex() forEach()
every() 對arr中所有元素進行遍歷, 同時接收一個條件函數, 判斷arr中的每一項是否都滿足該函數內條件,若全部元素都滿足, 則every()執行完成後返回true, 否則返回false
find() 對arr中所有元素進行遍歷,同時接收一個條件函數, 查找arr中符合條件的第一個元素,如果找到就返回這個元素,遍歷完還未找到,返回undefined
findIndex()和find()類似, 不過返回的是第一個元素的索引, 無則返回-1
forEach() 和 map() 類似 相當於迭代器,在遍歷過程中把每個元素依次作用於傳入的函數, 不會返回新的數組,如果函數對遍歷元素有修改操作,則會直接改變原數組對應元素的值
tips:
findIndex()和indexOf()的區別
findIndex的判斷條件是不定的,取決於傳進去的條件函數。
indexOf則是直接在原數組裏檢測目標元素是否存在,相當於條件唯一。
閉包
未完待續
https://www.liaoxuefeng.com/wiki/1022910821149312/1023021250770016