在JavaScript中,數據可分為兩類,分別為原生數據類型和對象數據類型
對象數據類型,是一種復合型的數據類型,
它可以把多個數據放到一起,就好像一個籃子,這個籃子里面的每一個數據都可以看作是一個單元,它們都有自己的名字和值。
對象
var container = {
caoyao: "草葯",
renshen: "人參"
};
var container = {};
container.caoyao = "草葯";
container.renshen = "人參";
console.log(container.hyq); // 這個屬性不存在,返回undefined
// 獲取對象的屬性的值
console.log(container.caoyao);
console.log(container['caoyao']);
var prop = "caoyao";
console.log(container[prop]);
typeof() 返回數據類型
var a = "hyq";
var fun = function(){
}
console.log(typeof(a));
console.log(typeof(fun));
if(typeof(fun) == "function"){
console.log(true);
}
對象內容的遍歷
var LiBai = {
name : "李白",
age : 18,
eat : function(){
console.log("黃鶴樓");
}
}
for(var i in LiBai){
console.log(i);
console.log(LiBai[i]);
}
數組的4種定義方式
直接量定義數組
var arr = ["first", "second", "third"];
console.log(arr);
構造函數的方式創建的一個數組對象
在JavaScript中, 每個類型其實都有一個函數作為支撐。
在這個例子 中,Array也叫作構造函數。
var a = new Array();
console.log(a.length); // 0
其他兩種
var b = new Array(8);
console.log(b.length); // 8
console.log(b); // 這個數組的內部就是8個空元素,沒有東西,但是占據了內存空間
var c = new Array("first", "second", "third");
數組方法
push() 元素追加到數組末尾
var b = new Array(8);
b.push("蘋果");
b.push( "香蕉");
b.push( "牛油果");
b[0] = "火龍果";
console.log(b);
pop() 刪除數組尾端元素
var b = new Array(8);
console.log(b.length); // 8
console.log(b); // 這個數組的內部就是8個空元素,沒有東西,但是占據了內存空間
b.push("蘋果");
b.push( "香蕉");
b.push( "牛油果");
b[0] = "火龍果";
b.pop();
console.log(b);
splice() 插入、刪除或替換數組元素
splice方法用前兩個參數進行定位,余下的參數表示插入部分。
數組的下標位置默認 從0開始
第一個參數 代表 需要操作的數組的起始位置
第二個參數代表 要刪除元素的個數
余下的參數表示插入部分
var a = [1,2,3,4,5];
a.splice(2,1); // 從數組下標為2的元素開始,刪除1個元素。
console.log(a); // [1,2,4,5 ]
var a = [1,2,3,4,5];
a.splice(2,1,38,66); // 從數組下標為2的元素開始,先刪除1個元素,后插入38 和 66 兩個元素
console.log(a); // [1, 2, 38, 66, 4, 5]
jion() 數組轉換字符串
join方法可以把數組中的所有元素放入一個字符串。
元素是通過指定的分隔符進行分隔的,而這指定的分隔符就是join方法的參數。
var arr = [1,2,3];
var str = arr.join(",");
console.log(str); // 1,2,3
函數七重關
函數七重關之一(函數定義)
兩種定義方式
function myFun(){
document.write("我一定要成為一名前端工程師!")
}
var a = function(){
document.write("我一定要成為一名全棧工程師!")
}
區別:
第一種方法定義的函數,把調用語句放在前面,則可以 成功調用。
第二種方法定義的函數,把調用語句放在前面,會報錯。
Uncaught TypeError: a is not a function
原因:
第一種方法定義的函數,它會被提前加載,因此調用語句可以寫在函數的定義之前,因為那個時候函數已經被加載完畢了。
第二種方式定義的函數是不會被提前加載的。必須要執行到函數定義的語句才會加載這個函數。
因為在調用a函數的時候,a函數還沒有加載,強行調用一個不存在的函數自然是不被允許的!
myFun();
function myFun(){
document.write("我一定要成為一名前端工程師!")
}
a(); // 報錯
var a = function(){
document.write("我一定要成為一名全棧工程師!")
}
疑問:
為什么第一個a打印出來是undefined,而不是直接報錯 呢?
我之前遇到過這種 情況,就是引用一個從來沒有被定義過的變量
apple
,得到的結果是直接報錯。ReferenceError: apple is not defined因為我從來沒有在任何地方定義過一個apple變量。你剛才調用了一個還未被加載的函數,為什么會打印出undefined而不是報錯呢?
console.log(a); // undefined
var a = function(){
alert("天大寒,硯冰堅。");
}
console.log(a);
console.log(apple); // 報錯
回答:
函數有沒有被加載 與 變量有沒有被定義是不同的事情。
函數有沒有被加載,可以看成function有沒有被賦值給變量a。從代碼上來看,自然是沒有的。
也就是說,當調用變量a的時候,變量a並沒有被賦值。但是不管變量a有沒有被賦予一個function函數,我就問你一 個問題,a有沒有定義?
疑問:
定義是定義了,可是它並沒有被運行到啊!
回答:
這就要說到 JavaScript代碼的運行機制 了
JavaScript編譯原理
JavaScript代碼在運行之前會經過一個編譯的過程,而編譯有三個步驟。
var a = 10;
第一個步驟是分詞
JavaScript代碼其實就是由一句句話組成的, 分詞的目的是把這些代碼分解為一個個有意義的代碼塊。如果經過分詞的步驟,那么得到的結果就是 ‘ var、a、=、10、;’
第二個步驟是解析
由JavaScript編譯器對剛才分詞得到的一個個代碼塊進行解析,生成一棵抽象的語法樹(AST)。
JavaScript代碼是沒有辦法直接運行的,要想運行JavaScript代碼,就需要通過JavaScript編譯器對其進行編譯,只有編譯之后的代碼才可以被識別,然后通過JavaScript引擎執行代碼邏輯。
抽象語法樹是什么啊?
抽象語法樹定義了代碼本身,通過操作這棵樹可以精准地定位到 賦值語句、聲明語句和運算語句。
再來說說剛才的代碼,很明顯,這是一個賦值語句,當然,這也是一個定義的語句。我們通過JavaScript的解析器把它解析為一棵抽象樹。
- type: Program
- -body
- -#1
- type: VariableDeclaration
- -declarations
- -#1
- type: VariableDeclarator
- -id
- type: Identifier
- name: a
- -init
- type: Literal
- value: 10
- raw: 10
- kind: var
- sourceType: script
Program :程序
VariableDeclaration:變量聲明
‘var a = 10;’這句話是一個程序,程序的目的是進行一個變量的聲明。
declarations[1]是聲明數組,中括號里面寫了一個1,表示這個語句只聲明了一個變量。kind代表種類,表示用 var關鍵字聲明一個變量,
VariableDeclarator:變量聲明
id:變量名
identifier:標識符
init:初始化操作,將10賦 給變量a
不把10賦值給a,看看會怎樣?
如果沒有給變量a賦值,那么JavaScript的解釋器也會給變量a賦一 個初始值,null代表空。
注意:這里的null不要理解為JavaScript里面的數據類型null,而是語義上的空。實際上,在代碼執行的時候,變量a的值是undefined。
最后一個步驟,就是代碼生成。
JavaScript引擎會把在第二個步驟中生成的抽象語法樹進行轉換。也許最終生成出來的就是一些機器指令,創建了一個叫作a的變量並放在變量區,然后分配一些內存以存放這個變量,最后將數字10存儲在了變量a所在的地方。
if(false) {
var b = 20;
}
console.log(b); // undefined
在if判斷中,而if判斷的條件是false,所以這句話的確不會執行。
但是,執行代碼是在運行階段,在代碼的 分詞階段 和 解析階段,變量a依然會被獲取,並且系統會默認給它一個 undefined。
又因為變量a不是在某一個函數的函數體中,而是在全局作用域里面,所以console.log方法依然可以訪問這個變量,因此獲取變量a 的值就是undefined。
console.log(a); // undefined
var a = function(){
alert("天大寒,硯冰堅。");
}
console.log(a);
console.log(apple); // 報錯
接下來可以解釋之前的那個問題了:
第一次執行console.log方法的時候,變量a還沒有被賦值為一個函數,但是JavaScript引擎還是會把它提取出來並放入全局作用域,並且默認給它一個undefined。
所以,第一次打印出來的就是undefined。接下來就是一個賦值語句了。
這個賦值語句把一個匿名函數賦給了變量a,那么從此變量a就指 向了這個函數,換句話說,一個名字叫作a的函數就已經產生了。
這句話一旦執行,a就不再是undefined了,而是一個函數。
接下來執行第二個console.log方法,這個時候a自然已經有值了,所以打印出來的是一個函數。
函數七重關之二(作用域)
在JavaScript中,作用域分為兩種,一種是全局作用域,另一種是函數作用域。
不管是全局作用域還是函數作用域,都被定義在 詞法階段。
詞法階段就是剛才所說的JavaScript編譯代碼的第一個步驟 ——分詞。所以,詞法階段也叫作分詞階段。
var a = 1;
function test(){
var a;
var inner = function(){
console.log(a);
}
inner();
}
test(); //undefined
函數作用域里面嵌套了函數作用域,那 么在最里面的inner函數中訪問一個變量,就會優先在inner函數里面尋找,結果卻發現找不到。
既然在當前函數作用域里面找不到,那么就往上翻一層,在它的父級作用域,也就是test函數的作用域里面尋找,結果發現找到了。
test函數里面定義了一個變量a,但是沒有賦值,那么a 就是undefined。
既然已經找到了,那么就不會去全局作用域里面尋找變 量a了。所以,全局作用域里面的變量a其實就是一個擺設。
函數七重關之三(參數傳遞)
function add(a,b,c){
var sum = a + b + c;
console.log(sum);
}
如果我在調用函數的時候就傳了一個參數咋辦?
function test(a){
console.log(a);
}
test(); // undefined
因為test函數明明是要求填寫一個參數的,那就是a。
可是在調用函數的時候,卻偏偏沒有參數傳遞進來。
這 按理說是不被允許的,可是當這種情況真的發生了會怎樣呢?
也就是說,如果沒有參數傳進來,那么函數中已經設置好的參數會等於什么 呢?
結果顯示:undefined。
其實,對於函數傳參到底是怎么回事,可以把這個例子再次細分。
剛才的函數中有一個參數a,那么這個參數自然也屬於函數作用域,就相當於下面這樣。
function test(){
var a;
console.log(a);
}
函數的參數可以簡單地 看成是在函數體,也就是花括號擴起來的地方,即里面的第一行定義了 一個變量。
因為我們並沒有給這個變量賦值,所以這個局部變量就是 undefined。
可以這么說,任何變量在被賦予真正的值之前,其在編譯階段都是undefined。
或者說,任何變量不管其最終的值是什么,它都曾經是undefined。
這些函數的參數可以被理解為一種預備變量。接下來說說正常的情況,比如我調用fun函數,傳遞一個參數18。傳參的過程就相當於是給預備變量賦值的過程。如果沒有傳參,那么預備變量自然還是 undefined。
function add(a,b,c){
var sum = a + b + c;
console.log(sum);
}
add(1); // NaN
這種情況下,a的值是1,b和c的值就是undefined,
那么數字1 和 2個undefined相加會是多少呢?
結果是NaN,代表無法計算。
如果我多傳一個參數又會怎樣呢?
好比我定義了一個 函數fun,但沒有參數,如果我在調用fun函數的時候故意給它加了一個參數,會發生什么?
結果是可想而知的,自然是什么都不會發生啦。
再回到剛才的例子中,就算你強行加了第四個參數,對結果也不會有什么影響。
function fun(){
}
fun(10);
function add(a,b,c){
var sum = a + b + c;
console.log(sum);
}
add(1,2,3,4); // 6
如果我一定要在函數里面訪問額外的參數需要咋辦?
其實所有的參數都會被裝載到函數內部一個叫 作arguments
的數組里面。
在函數的內部還維護了一個arguments數組。
function add(a,b,c){
console.log(arguments); // [1, 2, 3, 4, callee: ƒ, Symbol(Symbol.iterator): ƒ]
var sum = a + b + c;
console.log(sum);
}
add(1,2,3,4); // 6
傳過來的四個參數,其實都放進了這個默認的 arguments數組里面。
換句話說,參數列表里面的a、b、c也是根據這個數組賦值的。
function add(a,b,c){
console.log(arguments);
a = arguments[0];
b = arguments[1];
c = arguments[2];
var sum = a + b + c;
console.log(sum);
}
add(1,2,3,4); // 6
根據這個特性,可以完成一些有趣的功能,比如我可以編寫 一個函數,參數個數任意,實現數字的累加。
function add(){
if(!arguments[0]){
return 0;
}
for(var i = 1; i<arguments.length; i++){
arguments[0] = arguments[0] + arguments[i];
}
return arguments[0];
}
console.log(add(0,100,200,300)); //第一種累加器有bug, 第一個元素為0時,返回值都是0.
function add(){
var sum = 0;
for(var i = 0; i<arguments.length; i++){
sum = sum + arguments[i];
}
return sum;
}
console.log(add(0,100,200,300));
函數七重關之四(閉包)
function test(){
return function(){
}
}
在一個函數里面嵌套了另外一個函數,因為我只是想要把一個函數返回出去,而並不在乎這個內部函數叫啥名字, 所以干脆就不給它取名字了,那么里面的這個函數就是一個匿名的函數。
這種寫法雖然有點奇怪,但它依然是正確的。
因為函數作用域可以嵌套,所以里面的函數作用域 就可以訪問外面函數作用域中的變量了。
function test(){
var a = 0;
return function(){
console.log(a);
}
}
test()();
產生閉包的條件:
第一點,在函數內部也有一個函數。
第二點,函數內部的函數里面用到了外部函數的局部變量。
第三點,外部函數把內部函數作為返回值return出去 了。
閉包的好處:
正常情況下,我們調用一個函數, 其里面的局部變量會在函數調用結束后銷毀,這也是我們在全局作用域里面無法訪問函數局部變量的原因。
但是,如果你使用了閉包,那么就會讓這個局部變量不隨着原函數的銷毀而銷毀,而是繼續存在。
function test(){
var a = 0;
return function(increment){
a = a +increment;
console.log(a);
}
}
var inner = test();
inner(1); // 1
inner(1); // 2
inner(1); // 3
證明在每次調用內部函數的時候,里面訪問的都是同 一個變量a了。
這種寫法就相當於在全局作用域里面定義了一個變量a, 然后在函數中操作全局變量。
這樣的形式操作,也就是利用閉包 操作可以減少很多不必要的全局變量。
全局作用域是一塊公共區域,如果為了某個單一的功能而定義一個全局變量,則會導致全局變量過多, 代碼就變得一團糟了。
函數七重關之五(自執行函數)
所謂自執行函數,顧名思義, 就是在定義之后就立刻執行的函數,它一般是沒有名字的。
也正因為自執行函數沒有名字,所以它雖然會被立刻執行,但是它只會被執行一 次。
(
function(){
console.log(123);
}
)
();
自執行函數一般可以和閉包配合使用
var innner = (function(){
var a = 0;
return function(increment){
a = a + increment;
console.log(a);
}
})();
innner(2); // 2
innner(2); // 4
innner(2); // 6
函數七重關之六(“new”一個函數)
function hello(){
console.log(this);
}
hello();
window.hello();
window['hello']();
var p = 'hello';
window[p]();
打印出this對象
this也是JavaScript中的一個關鍵字,
this永遠指向當前函數的調用者。
this要么不出現,一旦出現,就一定出現在函數中。
this指向函數的調用者,換句話說,這個函數是誰調用的,那么this 就是誰。
JavaScript里面分為全局作用域和函數作用域,在全局 作用域里面定義的任何東西,不管是一個變量還是一個函數,其實都是 屬於window對象的。
function hello(){
console.log(this);
}
new hello(); // 產生一個新的對象
結果是 hello函數內部產生了一個新的對象 hello {},
也就是 hello函數的真實調用者——this關鍵字指向的那個對象。
說得簡單些,就是函數內部產生了一個新的對象,並且this指向了這個對象, 然后函數默認返回了這個新的對象。
function hello(){
console.log(this);
}
new hello;
var newObject = new hello();
console.log(newObject);
newObject就是函數里面的this,也就是函數內部新產生的那個對象了。
這種函數還有一個別稱,叫作 構造函數。
通過構造函數構建一個對象模板
指用一個函數的形式設計一種對象的種類。說得簡單些,比如蘋果、香蕉、板栗這些食物,它們都是食物,那么我就可以設計一個對象模板描述食物的一些共同特點。比如,食物有名字、味道、顏色等屬性,那么我就可以在函數中用this關鍵字設計這些屬性。
function Fruit(name, smell, color){
this.name = name;
this.smell = smell;
this.color = color;
}
一般來說,如果這是一個構造函數,那么首字母就需要大寫。
因為函數在使用了new關鍵字以后會從內部新產生一個對象出來, 而this就指向了這個對象。
基於這樣的一個緣由,可以直接在函數里面給未來即將生成的那個對象設置屬性。
function Fruit(name, smell, color){
this.name = name;
this.smell = smell;
this.color = color;
};
var apple = new Fruit('蘋果', '酸甜', '紅色');
如果需要兩個蘋果,就直接調用兩次new函數就行了。
var apple1 = new Fruit('蘋果', '酸甜', '紅色');
var apple2 = new Fruit('蘋果', '酸甜', '紅色');
使用 對象大括號寫法,則需要寫兩次。
var apple3 = {
name: "蘋果",
smell: "酸甜",
color: "紅色"
};
var apple4 = {
name: "蘋果",
smell: "酸甜",
color: "紅色"
}
錯誤的示范
apple3和apple4其實都是指向同一個蘋果的
var apple3 = {
name: "蘋果",
smell: "酸甜",
color: "紅色"
};
var apple4 = apple3;
驗證
添加一個 是否被吃的屬性,然后讓變量apple4等於apple3,修改apple4的eat屬性,把沒有被吃掉的狀態改成已經被吃掉。
結果 apple3 顯示也被吃了。
var apple3 = {
name: "蘋果",
smell: "酸甜",
color: "紅色",
isEat: false
};
var apple4 = apple3;
apple4.isEat = true; // 吃了apple4
console.log(apple3.isEat) // true
原因
除了基本數據類型之外,其他都屬於引用數據類型。
比如對象就屬於引用數據類型。
如果將基本數據類型賦值給某一個變量,然后將這個變量賦值給另外一個變量,就可以看成是數據值的復制。
var a1 = 10; var a2 = a1;
a1和a2還是不同的數據,雖然都是10,但是在內存上卻處於不同的空間。
而引用數據類型則不同,如果簡單地分出一個變量區和內存區,
apple3和apple4就都屬於變量區的兩個不同的變量了,但是卻指向同一塊內存地址,也就是真實的對象地址。
不管是apple3還是apple4,它們都擁有操作這一塊內存區域的權限,也就是說,它們都可以修改真實對象的屬性值。
函數七重關之七(回調函數)
所謂回調函數,就是指把一個函數的定義當作參數傳遞給另一個函數。
JavaScript提供了一種強大的特性,這個特性就是: 函數也可以作為另一個函數的參數。
function eat(food, callback){
callback(food);
}
eat ('羊肉串', function(food){
alert("坐在家里,自己動手烤着吃" + food);
})
變量和簡單數據類型
簡單數據類型,分為字符串、數值型、布爾型、null和undefined。
判斷為假的幾種情況:
字符串為空
數字 0
null
undefined
console.log('窮且益堅,不墜青雲之志。' ? '真' : '假'); // 真
console.log('' ? '真' : '假'); //假
console.log(10 ? '真' : '假'); //真
console.log(-10 ? '真' : '假'); //真
console.log(0 ? '真' : '假'); //假
console.log('0' ? '真' : '假'); //真
console.log(null ? '真' : '假'); //假
console.log(undefined ? '真' : '假'); //假
精度問題
在JavaScript中整數和浮點數都屬於 Number
數據類型,
所有數字都是以 64 位浮點數形式儲存,即便整數也是如此。
console.log(0.1+0.2); // 0.30000000000000004
計算過程
首先,十進制的0.1
和0.2
都會被轉換成二進制,但由於浮點數用二進制表達時是無窮的,例如。
0.1 -> 0.0001100110011001...(無限)
0.2 -> 0.0011001100110011...(無限)
IEEE 754 標准的 64 位雙精度浮點數的小數部分最多支持 53 位二進制位,所以兩者相加之后得到二進制為:
0.0100110011001100110011001100110011001100110011001100
因浮點數小數位的限制而截斷的二進制數字,再轉換為十進制,就成了 0.30000000000000004
。所以在進行算術計算時會產生誤差。
var num01 = 0.1;
var num02 = 0.2;
num01 = num01 * 10;
num02 = num02 * 10;
console.log((num01 + num02) / 10); // 0.3
封裝成函數,此函數只能適用與 兩個數 都是小數。
function add(num1, num2) {
// 將數字轉換為字符串
num1 = num1.toString();
num2 = num2.toString();
// 獲取數字小數點后的位數
var a = num1.split(".")[1].length;
var b = num2.split(".")[1].length;
var max = a; // 取最大的 小數位
if (a < b) { max = b; } // 根據位數 獲取對應的10次冪數
var beishu = 1;
for (var i = 0; i < max; i++) {
beishu = beishu * 10;
}
// 小數 乘 這個倍數
num1 = num1 * beishu;
num2 = num2 * beishu;
// 最后再除於 這個倍數
var sum = (num1 + num2) / beishu;
console.log(sum);
}
add(0.01, 0.2); // 0.21
這個函數現在雖然可以正確地計算0.1+0.2,但不能計算1+0.2。
下面是錯誤示例
在獲取小數位數之前判斷一下,如果是小數,就按照原先的方法進行,否則就默認小數位數是0!
function add(num1, num2) {
// 將數字轉換為字符串
num1 = num1.toString();
num2 = num2.toString();
var a, b;
// 判斷是否為小數
if(num1.indexOf('.') == -1){
a = 0;
}else{
a = num1.split(".")[1].length;
}
// 判斷是否為小數
if(num2.indexOf('.') == -1){
b = 0;
}else{
b = num2.split(".")[1].length;
}
// 取最大的 小數位
var max = a;
if(a < b){
max = b;
}
// 根據位數 獲取對應的10次冪數
var beishu = 1;
for(var i = 0; i < max; i++){
beishu = beishu * 10;
}
// 小數 乘 這個倍數
num1 = num1 * beishu;
num2 = num2 * beishu;
// 最后再除於 這個倍數
var sum = (num1 + num2) /beishu;
console.log(sum);
}
add(1.01, 2.02); // 3.03
add(1.01, 2.002); // 3.012
add(1.001, 2.002); // 3.0029999999999997
第三次執行 ,放大的倍數是1000,也就是說,1.001 乘以1000本身就有問題。
console.log(1.001 * 1000); // 1000.9999999999999
看來不僅是加減法,小數乘除法的計算依然會有精度丟失的問題!
解決方法:
那就不作乘法,而是利用替換。
function add(num1, num2){
// 先將數字轉換為字符串
num1 = num1.toString();
num2 = num2.toString();
var ws1 = 0;
var ws2 = 0;
// 如果小數點存在,則獲取小數位數
if(num1.indexOf('.') != -1){
ws1 = num1.split('.')[1].length;
}
if(num2.indexOf('.') != -1){
ws2 = num2.split('.')[1].length;
}
// 看誰的小數位 大,誰的小
var bigger = (ws1>ws2) ? ws1 : ws2;
var smaller = (ws1<ws2) ? ws1 : ws2;
// 計算 需要補齊幾個 0的個數。
var zerosCount = bigger - smaller;
// 先全部去除小數點
num1 = num1.replace(".", "");
num2 = num2.replace(".", "");
// 誰的尾數 最小,就給誰補0;
if(ws1 == smaller){
for(var i = 0; i < zerosCount; i++){
num1 += "0";
}
}else{
for(var i = 0; i < zerosCount; i++){
num2 += "0";
}
}
// 開始計算,字符串轉換為整數
var sum = parseInt(num1) + parseInt(num2);
// 按照最大的小數位,計算倍數
var beishu = 1;
for(var i = 0; i < bigger; i++){
beishu = beishu * 10;
}
return sum = sum / beishu;
}
console.log(add(1, 2.009)); // 3.009
字符串方法
split() 分隔字符串
split函數,可以把一個字符串通過某種規則和標記符號進行分隔,並返回一個數組。
var num01 = "0.01";
var num01_arr = num01.split(".");
console.log(num01_arr); // ['0', '01']
indexOf() 搜索字符串
indexOf的意思是在原字符串中搜索一個特定的字符串,
比如從“123456”中搜索“2”,如果存在“2”,就把這個字符串在原字符串中的位置返回。
字符串的下標也是從0開始的。因此,用indexOf方法從“123456”中搜索“2”,返回的結果是1,而不是2。
如果搜索不到則 返回 -1 。
if('1.002'.indexOf('.') == -1){
a = 0;
}else{
a = num1.split(".")[1].length;
}
replace() 替換單個字符串
把里面的c替換成大寫的C
"abcd".replace("c", "C");
第一個參數是需要替換的內容,第二個參數是替換后的內容。
但是這個方法有一個缺憾,那就是它只能匹配到字符串中的第一個匹配項,
也就是說,如果原字符串里面有多個匹配項,那么就只有第一個匹配項會生效,剩余的匹配項則享受不到替換的待遇。
比如,現在需要對字符串“abcdabcd”進行替換,將里面所有的a替換為A,如果單純用replace方法,效果是不盡人意的。
"abcdabcd".replace("a", "A"); \\ "Abcdabcd"
jQuery
引用jQuery
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
$是什么?
console.log(typeof($)); //function
第一步是用jQuery選擇器獲取這個按鈕;第二步是給這個按鈕添加一個單擊事件。
<body>
<button id="btn">點我</button>
</body>
<script>
console.log(typeof($)); //function
$('#btn').click(function func(){
alert('現在時間:2021/11/24 23:48,明天取爬梅林咯!');
});
</script>
疑問?
為什么要把用jQuery添加單擊事件的代碼放在input標簽下面呢?如果放在上面行不行?
因為瀏覽器在解析代碼時是一行一行地往下執行的,當它執行到這一段代碼的時候。
$('#btn').click(function func(){ alert('現在時間:2021/11/24 23:48,明天取爬梅林咯!'); });
如果JavaScript代碼下方的按鈕還沒有被加載,那么$('#btn')
就沒有辦法獲取對應的按鈕對象。
解決辦法
寫一個DOM加載完畢后的監聽函數
<script>
$(document).ready(function() {
// 這里的代碼會在所有元素加載完畢后再執行
$('#btn').click(function func(){
alert('不積跬步,無以至千里;不積小流,無以成江海。 -- 荀子');
});
});
</script>
<body>
<button id="btn">點我</button>
</body>
$(document).ready方法接收一個回調函數,它會等頁面上所有的DOM資源(不包括圖片這種占用帶寬的資源)全部加載完畢后,再調用這個回調函數。這樣一來,就不用考慮在綁定事件的時候某個HTML元素還沒有被加載的情況了。
jQuery操作DOM
查找元素
find() 查找元素
eq() 從元素集中查詢元素,指定元素位置(最小為0),如果是負數則從集合中的最后一個元素往回計數
text() 返回元素的文本
<body> <h2>道德經</h2> <ul> <li>道可道,非常道</li> <li>名可名,非常名</li> <li>無名天地之始,有名萬物之母</li> <li>故常無欲,以觀其妙</li> <li>常有欲,以觀其徼(jiào)</li> <li>此兩者同出而異名,同謂之玄,玄之又玄,眾妙之門</li> </ul></body><script> var ul = $('ul'); var lis = ul.find('li'); var li2 = lis.eq(2); // 指定元素位置(最小為0),如果是負數則從集合中的最后一個元素往回計數 var li2_text = li2.text(); console.log(li2_text); // 無名天地之始,有名萬物之母</script>
第二種寫法
<script> var text = $('ul li:eq(1)').text(); console.log(text); // 名可名,非常名</script>
查詢屬性
attr() 設置或返回被選元素的屬性值。
尋找最后一個li元素的id屬性值
<body>
<h2>道德經</h2>
<ul>
<li id="a1">道可道,非常道</li>
<li id="a2">名可名,非常名</li>
<li id="a3">無名天地之始,有名萬物之母</li>
<li id="a4">故常無欲,以觀其妙</li>
<li id="a5">常有欲,以觀其徼(jiào)</li>
<li id="a6">此兩者同出而異名,同謂之玄,玄之又玄,眾妙之門</li>
</ul>
</body>
var $li = $('ul li');
var len = $li.length;
var id = $li.eq(len-1).attr('id');
alert(id); // a6
第二種方式
用jQuery的一種特殊選擇器直接獲取最后一個元素
var id = $('ul li:last').attr('id');
alert(id); //a6
鏈式調用
剛才的代碼中頻繁出現對象在調用函數之后,又立刻調用其他函數或者屬性的情況,你可知道這是怎么回事,為什么能夠這么寫?
如果我要調用func01函數和func02函數,一般是這么寫。
這是正常的函數調用,不屬於鏈式調用。
var myFunction = function() {
return{
func01 : function() {
console.log("func01");
},
func02 : function() {
console.log("func02");
}
}
}
var obj = myFunction();
obj.func01();
obj.func02();
如果是鏈式調用,就需要寫這樣的代碼。
obj.func01().func02();
既然能夠在調用func01函數之后立刻調用func02函數,就需要func01函數的返回值是一個對象,
func01函數和func02函數的宿主對象都是obj,也就是說,我只要讓func01的返回值變成obj就行了。
一個this關鍵字就可以解決這個問題,因為在函數中,this 關鍵字永遠指向當前函數的調用者。
var myFunction = function() {
return{
func01 : function() {
console.log("func01");
return(this);
},
func02 : function() {
console.log("func02");
return(this);
}
}
}
var obj = myFunction();
obj.func01().func02();
創造新的元素
appendTo()
append()
jQuery還可以動態地創造新的元素並添加到頁面上
如果我想添加一個新的li 到ul列表中
<body>
<h2>道德經</h2>
<ul>
<li id="a1">道可道,非常道</li>
<li id="a2">名可名,非常名</li>
<li id="a3">無名天地之始,有名萬物之母</li>
<li id="a4">故常無欲,以觀其妙</li>
<li id="a5">常有欲,以觀其徼(jiào)</li>
</ul>
</body>
首先, 創建一個新的元素。
var newLi = $("<li id=\"a6\">此兩者同出而異名,同謂之玄,玄之又玄,眾妙之門</li>");
用到了 jQuery的工廠函數$(),當向這個函數傳入一段HTML代碼時,jQuery會自動解析這一段HTML,然后創建對應的DOM節點,最后將這個DOM節點的jQuery對象返回出去。
接下來我只需要把它添加到ul元素中即可。可以使用appendTo方法或者append方法。
用appendTo方法
newLi.appendTo($('ul'));// 簡寫
$("<li id=\"a6\">此兩者同出而異名,同謂之玄,玄之又玄,眾妙之門</li>").appendTo($('ul'));
用append方法
$('ul').append($("<li id=\"a6\">此兩者同出而異名,同謂之玄,玄之又玄,眾妙之門</li>"));
還有一種html方法,它的意義和添加方法有所不同,是直接替換目標元素里面的所有HTML代碼。
$('ul').html($("<li id=\"a6\">來自老子</li> "));
這樣一來,ul 里面就只剩下一個 li 元素了。
拓展:四個插入函數
insertAfter() 把匹配的元素插入另一個指定元素集合的后面
insertBefore() 把匹配的元素插入另一個指定元素集合的前面
prepend() 向匹配元素集合中的每個元素的開頭插入由參數指定的內容
prependTo() 向目標的開頭插入匹配元素集合中的每個元素
<body>
<p>老驥伏櫪,志在千里。</p>
<div>
志在千里
</div>
</body>
<script>
$('<h4>來自--曹操</h4>').insertAfter('p');
$('<h3>龜雖壽</h3>').insertBefore('p');
$('div').prepend('<span>老驥伏櫪,</span>'); //在div內部
$('<h3>龜雖壽</h3>').prependTo('div'); //在div內部
</script>
最后DOM結構變成下面這樣子:
<body>
<h3>龜雖壽</h3>
<p>老驥伏櫪,志在千里。</p>
<h4>來自--曹操</h4>
<div>
<h3>龜雖壽</h3>
<span>老驥伏櫪,</span>
志在千里
</div>
</body>
刪除和隱藏節點
remove() 刪除
hide() 隱藏
show() 顯示
<body>
<h2 id="a1">我一定一定要學會前端!</h2>
</body>
<script>
$("#a1").remove(); // 刪除
$("#a1").hide(); // 隱藏
$("#a1").show(); // 顯示
</script>
jQuery操作屬性
attr() 返回或設置元素的屬性
<h1 id="d1">道可道,非常道。</h1>
給attr方法傳入一個參數,那么就獲取這個元素的某個屬性的值,屬性名就是傳進來的參數。
var id = $("#d1").attr('id'); // d1
給attr方法傳入兩個個參數, 可以設置元素的屬性和屬性值。
$("#d1").attr('id', 'd2');
var id = $("#d2").attr('id'); // d2
給元素設置多個屬性,就需要給attr方法傳入一個JavaScript對象。
$('#d1').attr({name:'ddj', title:'道德經'});
var title = $("#d1").attr('title'); // 道德經
刪除屬性
removeAttr() 刪除屬性
刪除上面設置的name屬性
$('#d1').removeAttr('name');
內容操作
如何用jQuery設置和獲取HTML、文本和值?
html() 獲取或設置 元素內部的html代碼
text() 獲取或設置 元素內部的文本內容
val() 獲取或設置 元素的值
如果不傳入參數, 那么就獲取元素內部的html代碼或者文本內容。
如果傳入參數,則是替換 或 賦值 的意思。
<script>
$(document).ready(function(){
var html_text = $("#d1").html();
alert(html_text); // 道可道,非常道。<span>--道德經</span>
var text = $("#d1").text();
alert(text); // 道可道,非常道。--道德經
});
</script>
<body>
<h1 id="d1">道可道,非常道。<span>--道德經</span></h1>
</body>
傳入參數
$("#d1").html('<strong>老子</strong>'); //頁面只顯示 老子 兩字
$("#d1").text('<strong>老子</strong>'); //頁面顯示 <strong>老子</strong>
獲取和設置value 值的相關操作
<label for="">姓名:<input type="text"></label>
<input type="button" onclick="get_name()" value="獲取名字">
<script>
function get_name(){
var name = $('input:eq(0)').val();
alert(name);
}
</script>
val方法不僅可以操作input元素,還可以操作下拉框(select)、多選框(checkbox)和單選按鈕(radiobox)。
<select id="fruits">
<option>西瓜</option>
<option>香蕉</option>
<option>蘋果</option>
</select>
設置默認顯示為 香蕉
$("select:eq(0)").val('香蕉');
如果option 設置了value,那就要設置為value值。
設置默認顯示為 蘋果
<select id="fruits">
<option value="01">西瓜</option>
<option value="02">香蕉</option>
<option value="03">蘋果</option>
</select>
<script>
$("select:eq(0)").val('03');
</script>
如果希望同時選擇香蕉和蘋果,那么就是多選下拉框,需要給這個select標簽設置一個multiple。
<select id="fruits" multiple="multiple">
<option value="01">西瓜</option>
<option value="02">香蕉</option>
<option value="03">蘋果</option>
</select>
需要給val 方法傳入一個數組.
<script>
$("select:eq(0)").val(['02', '03']);
</script>
遍歷和尋找節點
jQuery遍歷節點的操作有哪些,如何使用?
jQuery遍歷節點的操作
children()
這個方法可以獲取某個元素的下一代子元素,但不包括孫子輩的元素。該方法只沿着DOM樹向下遍歷單一層級。
parent() 查找 父節點
prev() 查找 上一個 兄弟節點
next() 查找 下一個 兄弟節點
siblings() 返回所有 兄弟元素
希望通過ul元素獲取其中的5個孩子節點
<ul id="menu">
<li>豫章故郡,洪都新府。</li>
<li>星分翼軫,地接衡廬。</li>
<li>襟三江而帶五湖,控蠻荊而引甌越。</li>
<li>物華天寶,龍光射牛斗之墟;</li>
<li>人傑地靈,徐孺下陳蕃之榻。 <strong>出自 滕王閣序</strong></li>
</ul>
<script>
var lis = $("#menu").children();
console.log(lis);
</script>
現在我想找到 strong 這個標簽
var lis = $("#menu").children();
var b= lis.last().find('strong');
console.log(b);
我現在想通過 li 節點 找到它的父節點
var li1 = $('li:eq(0)');
var ul = li1.parent();
console.log(ul);
通過某一個節點找到它的兄弟節點
現在給每個標簽設置一個id
<ul id="menu">
<li id="a1">豫章故郡,洪都新府。</li>
<li id="a2">星分翼軫,地接衡廬。</li>
<li id="a3">襟三江而帶五湖,控蠻荊而引甌越。</li>
<li id="a4">物華天寶,龍光射牛斗之墟;</li>
<li id="a5">人傑地靈,徐孺下陳蕃之榻。 <strong>出自 滕王閣序</strong></li>
</ul>
var a3 = $("#a3");
var a2 = a3.prev();
var a4 = a3.next();
console.log(a2.html()); //星分翼軫,地接衡廬。
console.log(a4.html()); //物華天寶,龍光射牛斗之墟;
根據一個元素找到和它同一級別的所有兄弟元素。
var lis = a3.siblings();
console.log(lis); // [li#a1, li#a2, li#a4, li#a5]
Vue
引入文件
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
數據綁定
<div id="app">
<input type="text" v-model="message">
<p>{{message}}</p>
</div>
<script>
new Vue({
el: '#app',
data: {
message: '小賊,哪里跑!'
}
})
</script>
v-model通常用於表單組件的綁定,如input和select等,
v-model與v-text的區別在於它實現的是表單組件的雙向綁定,
v-model用於表單控件,其他的標簽是沒有用的。
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<style>
table,tr,td {
border: 1px solid red;
border-collapse: collapse;
}
tr:last-child td{
text-align: right;
}
tr:last-child td span{
color: red;
}
</style>
<table id="app">
<tr>
<th colspan="2">登錄界面</th>
</tr>
<tr>
<td>請輸入用戶名:</td>
<td><input type="text" v-model='username'></td>
</tr>
<tr>
<td>請輸入密碼:</td>
<td><input type="password" v-model='pwd'></td>
</tr>
<tr>
<td colspan="2">
<span v-text="errMsg"></span>
<input type="submit" value="登錄">
</td>
</tr>
</table>
<script>
var vue = new Vue({
el: '#app',
data: {
username: '胡大大',
pwd: '88888888',
errMsg: '用戶名或密碼錯誤'
}
});
</script>
事件綁定
v-on監聽事件
語法糖
v-on:click 簡寫成 @click
<table id="app">
<tr>
<th colspan="2">登錄界面</th>
</tr>
<tr>
<td>請輸入用戶名:</td>
<td><input type="user" v-on:focus="handleFocus" v-on:blur="handleBlur" v-bind:style="userNameStyle" v-model='username'></td>
</tr>
<tr>
<td>請輸入密碼:</td>
<td><input type="password" v-model='pwd'></td>
</tr>
<tr>
<td colspan="2">
<span v-text="errMsg"></span>
<input type="submit" value="登錄">
</td>
</tr>
</table>
<script>
var vue = new Vue({
el: '#app',
data: {
username: '胡大大',
pwd: '88888888',
errMsg: '',
userNameStyle: {}
},
methods: {
handleFocus: function(){
console.log('獲得焦點!');
this.userNameStyle = {
width: '50px',
borderColor: 'pink'
}
},
handleBlur: function(){
console.log('失去焦點!');
this.userNameStyle = {};
}
}
});
</script>
事件冒泡
.stop 停止冒泡
.prevent 阻止提交,.prevent只對form和a標簽有效
.capture 優先觸發
.self 只允許元素自己觸發,子元素無法觸發。
.once 只觸發一次
事件冒泡的概念
假設現在有這樣的情況:有2個div,父div嵌套了子div;父div有一個單擊事件,子div也有一個單擊事件。當單擊子div的時候,因為子div在父div里面,頁面又不知道你到底想單擊子div還是父div,那么咋辦呢?結果就是先觸發子div的單擊事件,再觸發父div的單擊事件。
先初始化代碼
<td colspan="2" @click="modelClick">
<span></span>
<input type="submit" value="登錄" @click="btnClick">
</td>
var vue = new Vue({
el: '#app',
methods: {
modelClick: function() {
console.log('modelClick');
},
btnClick: function() {
console.log('btnClick');
}
}
});
單擊登錄按鈕,將按順序顯示
btnClick
modelClick
給 **登錄 **按鈕加上.stop
,就不會觸發 父元素的click事件了。
<td colspan="2" @click="modelClick">
<span></span>
<input type="submit" value="登錄" @click.stop="btnClick">
</td>
單擊登錄按鈕,效果是只會打印btnClick,不會打印modelClick
btnClick
form表單組件和a鏈接組件都會導致頁面刷新和跳轉。
如果不希望頁面刷新,則可以加上.prevent以阻止這種默認的刷新操作。
注意:.prevent只對form和a標簽有效。
.prevent
<td colspan="2" @click="modelClick">
<span ></span>
<input type="submit" value="登錄" @click.stop="btnClick">
<a href="http://baidu.com" @click.prevent>baidu</a>
</td>
使用.prevent
后單擊 baidu 將不會跳轉
.capture
在外層td元素上添加這個修飾符,則會優先觸發modelClick,然后觸發按鈕自己的單擊事件,這就是一個優先級的調整
<td colspan="2" @click.capture="modelClick">
<span ></span>
<input type="submit" value="登錄" @click="btnClick">
</td>
單擊登錄按鈕,將按順序顯示
modelClick
btnClick
.self
事件修飾符.self的作用是:當僅單擊元素本身時,只允許元素自己觸發,子元素無法觸發。
在外層td元素上添加這個修飾符,當單擊外層td的部分(不單擊按鈕部分)時,就會只觸發modelClick,不會觸發按鈕自己的單擊事件,
簡單了解這個修飾符即可,因為這種行為本來就是默認的。
<td colspan="2" @click.self="modelClick">
<span ></span>
<input type="submit" value="登錄" @click="btnClick">
</td>
意外發現
當我單擊 登錄按鈕時,只會觸發btnClick,相當於使用.stop
.once
事件修飾符.once表示只觸發一次
給td的單擊事件加上.once修飾符,那么modelClick就只會觸發一次。
<td colspan="2" @click.once="modelClick">
<span ></span>
<input type="submit" value="登錄" @click="btnClick">
</td>
無論我單擊多少次 td,控制台只打印了一次modelClick。
條件語句
v-if
v-else
v-else-if
當第一次單擊“登錄”按鈕的時候,“登錄”按鈕上面的“登錄”二字變成“登錄中…”的字樣。當后台成功返回數據或者超時后,就在頁面上顯示相應的信息,同時讓“登錄”按鈕再變回原樣。
<td colspan="2">
<span v-text="errMsg"></span>
<input v-if="!isLogining" type="submit" value="登錄" @click="btnClick">
<input v-else type="submit" value="登錄中...">
</td>
var vue = new Vue({
el: '#app',
data: {
isLogining: false,
errMsg: ""
},
methods: {
btnClick: function() {
console.log('btnClick');
this.isLogining = true;
}
}
});
一般這個時候,前端就會采用ajax等技術向后台發起登錄請求了。
后台過一會兒,就會返回一個響應,告訴前端頁面是否登錄成功。
下面用延時函數模擬一下即可
錯誤案例:
btnClick: function() {
console.log('btnClick');
this.isLogining = true; // 模擬發起請求,2秒后后台返回結果
setTimeout(function() {
alert(this); // 這里的 this 指向 window對象
this.errMsg = "登錄失敗,請檢查密碼是否正確!";
this.isLogining = false;
}, 2000);
}
結果發現,彈窗成功彈出來了,說明setTimeout起作用了,那么就可以確定回調函數的代碼出了問題。
原因是這里的 this 指向的是 window對象,而非Vue對象。
setTimeout 等價於 window.setTimeout
解決辦法
因為最上面定已經把Vue對象 賦值給了vue變量。
直接使用使用vue變量,但這並不是常見的解決辦法。
window.setTimeout(function() {
vue.errMsg = "登錄失敗,請檢查密碼是否正確!";
vue.isLogining = false;
}, 2000);
·在能獲取this的地方新定義一個that變量,將Vue對象存起來,
然后在回調函數里面調用即可,這種做法是較為普遍的。
var that = this;
setTimeout(function() {
that.errMsg = "登錄失敗,請檢查密碼是否正確!";
that.isLogining = false;
}, 2000);
循環語句
v-for
<tr>
<td>請選擇身份:</td>
<td>
<select v-model="role">
<option v-for="(item, index) in roleList" :value="item.value" :id="index">{{item.label}}</option>
</select>
</td>
</tr>
var vue = new Vue({
el: '#app',
data: {
role: '1', // 默認游客
isLogining: false,
errMsg: "",
roleList: [
{value:'1', label:'我是游客'},
{value:'2', label:'我是普通用戶'},
{value:'3', label:'我是管理員'},
]
}
});
屬性綁定
v-bind做屬性綁定
v-bind:xxx簡寫成:xxx
<option v-for="(item, index) in roleList" :value="item.value" :id="index">{{item.label}}</option>
綁定之后才能接收 data里面的 數據
Vue組件開發
為什么要用組件?
如果哪天又要改動按鈕的顏色,咋辦?甚至需要制作一個按鈕組。比如,紅色的按鈕用來刪除,藍色的按鈕用來新增,黃色的按鈕用來修改。這一系列的要求是很常見的,我們可以使用Vue的相關語法制作一套按鈕組件,以完成這樣的需求,同時也可以大大提升程序的健壯性和可擴展性。
組件也可以成為一種模板,你只需要照着這個模板把需要的參數傳進去就能得到想要的效果。
組件定義在 components 屬性中,
components屬性是和data與methods平級的,代表當前Vue對象的局部組件。
var vue = new Vue({
el: '#app',
data: {}, // 局部組件
components: {}
});
按鈕的代碼需要放在template 模板里面,
注意:雙引號需要轉義
定義一個coolBtn的組件模板
// 局部組件
components: {
'coolBtn': {
template: "<input type=\"submit\" value='提交' style=\" background-color: deeppink; color: #fff;border: none;padding: 2px 20px;border-radius: 6px;margin: 2px 6px;\">"
}
}
如何使用 組件模板呢?
在視圖里面就可以直接調用這個組件了,調用的方式就和寫普通的HTML標簽一樣,但是標簽名要和組件名稱保持一致。
錯誤寫法:
<coolBtn></coolBtn>
這是一種駝峰式的命名法。對於這種駝峰式的命名,在調用組件的時候需要格外注意,每次要換成大寫字母的地方都需要額外添加一個半字線(-),然后大寫字母還是轉變成小寫字母,像這樣:
<td colspan="2">
<span></span>
<cool-btn></cool-btn>
<input v-else type="submit" value="登錄中...">
</td>
組件 傳參
對於一個組件來說,按鈕具體顯示什么文字應該交給按鈕的調用者決定。
因此,需要給按鈕組件添加一個name 屬性。
該如何給自定義組件添加屬性呢?
在具體的組件配置項中添加一個props屬性就可以了,props屬性的值是一個數組,數組里面存放的就是當前組件可以接收的屬性名稱。
components: {
'coolBtn': {
props: ['name'],
template: "<input type=\"submit\" value='提交' style=\" background-color: deeppink; color: #fff;border: none;padding: 2px 20px;border-radius: 6px;margin: 2px 6px;\">"
}
}
傳進來的屬性名為name,這個name該如何插入template中呢?
對於input來說,它也是一個組件。我們的coolBtn組件無非就是對input組件做了一次拓展開發而已。input也是組件,那么它的value就是屬性,Vue綁定屬性的方式就是在左邊加一個冒號,於是可以這樣修改template。
:value='name'
components: {
'coolBtn':{
props: ['name'],
template:"<input type=\"submit\" :value='name' style=\" background-color: deeppink; color: #fff;border: none;padding: 2px 20px;border-radius: 6px;margin: 2px 6px;\">"
}
}
最后我們給coolBtn傳遞name屬性值就行
<cool-btn name="登錄"></cool-btn>
注意,我們給coolBtn組件添加的屬性是普通的屬性,而不是Vue的屬性綁定。
如果在name的左側加一對冒號,又會發生什么呢?
<cool-btn :name="登錄"></cool-btn>
結果報錯了!
原因:一旦我們寫成了:name,那么就說明我們希望給name屬性做Vue的屬性綁定,它會默認在data里面尋找是不是有一個叫作 登錄 的數據變量。
如果找不到,就直接給你報錯。
如果我們只是想要傳遞一個字符串進去,就可以直接用不帶冒號的name,或者這樣寫:
<cool-btn :name= "'登錄'"> </cool-btn>
依然是用了Vue的數據綁定,但我們只是直接寫了一個直接量進去而已。
組件事件
我們自定義的按鈕組件也是一個Vue組件,那么就完全可以給它添加methods屬性,然后把它對應的單擊事件寫進去就行了。
注意:這個methods是coolBtn里面的methods,不是外面Vue對象的methods,不要混淆。
最后 再給組件模板添加單擊事件。
// 局部組件
components: {
'coolBtn': {
props: ['name'],
template: "<input @click='defaultClick' type=\"submit\" :value='name' style=\" background-color: deeppink; color: #fff;border: none;padding: 2px 20px;border-radius: 6px;margin: 2px 6px;\">",
methods: {
defaultClick: function() {
alert("我被點擊了哦!");
}
}
}
}
測試一下,單擊“登錄”按鈕,應該能看到一個默認的彈窗。
但是這樣不行啊,因為在更多的時候組件是被封裝好的,根本不涉及業務邏 輯。
我們希望調用coolBtn組件制作一個登錄按鈕,但是登錄的邏輯是外面Vue組件需要做的事情,不能寫在按鈕組件里面。也就是說,我們希望在子組件的defaultClick方法里面調用外面的某個方法。調用外面的什么方法呢?比如在登錄的時候隨便寫了一個方法叫作login,
var vue = new Vue({
el: '#app',
data: {
role: '1', // 默認游客
isLogining: false,
errMsg: "",
roleList: [{
value: '1',
label: '我是游客'
}, {
value: '2',
label: '我是普通用戶'
}, {
value: '3',
label: '我是管理員'
}, ]
},
methods: {
login: function() {
alert("登錄成功!");
}
},
// 局部組件
components: {
'coolBtn': {
props: ['name'],
template: "<input @click='defaultClick' type=\"submit\" :value='name' style=\" background-color: deeppink; color: #fff;border: none;padding: 2px 20px;border-radius: 6px;margin: 2px 6px;\">",
methods: {
defaultClick: function() {
alert("我被點擊了哦!");
}
}
}
}
});
子組件怎么知道將來需要調用一個叫作login的方法呢?
這就需要遵循一個原則 — 約定大於配置。
就是封裝好coolBtn組件,因為是按鈕,所以肯定要被單擊,單擊就要調用單擊事件。那么不妨就規定好, 當coolBtn組件觸發默認的defaultClick方法時,就觸發調用者的某個方法,
defaultClick: function() {
alert("我被點擊了哦!"); // 發送 btn-click事件。
this.$emit('btn-click');
}
this.$emit('btn-click') 這句代碼的含義是觸發該組件綁定的btn- click事件所對應的來自於父組件的方法。
$emit 相當於 發送事件, 事件名則是自定義的,
這句話有點繞,再加一點代碼吧,這樣一來,coolBtn的調用就要這么寫了
<cool-btn name="登錄" @btn-click="login"></cool-btn>
在調用coolBtn的時候新添加了一個btn-click事件,
因為對於父頁面來說,cool-btn就是一個組件而已,當然可以給一個組件綁定事件。
只不過這個事件叫作btn-click,不是我們熟悉的click、blur、focus等。
btn-click是子組件約定的,只要觸發btn-click事件,子組件就會相應地觸發內部的defaultClick。
btn-click就是一個中轉站,我們最終的目的是單擊coolBtn內部的input按鈕。
但input按鈕在coolBtn的內部被封裝起來了,我們沒辦法直接控制它。
一個優秀的組件必須給外部提供完整的訪問權限。
一個好的組件還必須擁有良好的擴展性和選擇性。
我們還可以設置按鈕的顏色,傳遞一個color屬性進去即可。但是調用者還是更喜歡直接得到幾個選項自己選擇。
先設置幾個默認的顏色
.primary {
background: #400eff;
;
}
.danger {
background: #f56c6c;
;
}
.success {
background: #67c23a;
;
}
.warning {
background: #9e9a95;
;
}
然后給組件增加一個type屬性
props: ['name', 'type'],
同樣,在template里面也要增加這個class選項。
template: "<input @click='defaultClick' :class='type' type=\"submit\" :value='name' style=\" color: #fff;border: none;padding: 2px 20px;border-radius: 6px;margin: 2px 6px;\">",
這樣一來,組件的調用者只需要傳入一個type屬性,即可同步更新template里面的class的值。
完整代碼如下:
// 局部組件
components: {
'coolBtn': {
props: ['name', 'type'],
template: "<input @click='defaultClick' :class='type' type=\"submit\" :value='name' style=\" color: #fff;border: none;padding: 2px 20px;border-radius: 6px;margin: 2px 6px;\">",
methods: {
defaultClick: function() {
alert("我被點擊了哦!"); // 發送 btn-click事件。
this.$emit('btn-click');
}
},
created: function() {
// alert('按鈕初始化');
if (!this.type) {
this.type = 'primary';
}
}
}
}
調用組件按鈕
<cool-btn name="登錄":type="'success'" @btn-click="login"></cool-btn>
全局組件component
最后提一句,這個組件是一個局部組件,就是說這個組件因為是在當前頁面的Vue對象里面定義的,所以只有在這個頁面才能使用。
其他頁面要想使用這個組件,只能把組件的源代碼重新寫一遍。很明顯,這樣非常不利於組件的維護和更新,萬一對組件進行了修改或者擴展,就需要到每一個使用該組件的頁面中修改組件對應的源代碼,這是非常麻煩的。所以,我們可以將這個按鈕組件升級為全局組件。
注意:定義全局過濾器或全局組件一定要寫在 創建Vue對象 的前面!
// 全局組件
Vue.component('coolBtn', {
props: ['name', 'type'],
template: "<input @click='defaultClick' :class='type' type=\"submit\" :value='name' style=\" color: #fff;border: none;padding: 2px 20px;border-radius: 6px;margin: 2px 6px;\">",
methods: {
defaultClick: function() {
alert("我被點擊了哦!"); // 發送 btn-click事件。
this.$emit('btn-click');
}
},
created: function() {
// alert('按鈕初始化');
if (!this.type) {
this.type = 'primary';
}
}
});
相當於在總的Vue大對象上添加了一個組件,我們不妨新建一個coolBtn.js,然后把這些代碼復制進去。
注意,因為這個全局組件需要依賴Vue的庫文件,所以必須先引入Vue的核心文件,再引入coolBtn.js。
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script><script src="./js/coolBtn.js"></script>
計算屬性computed
Vue的計算屬性是指在某些時候需要在頁面上動態計算一些值,這些計算的過程可以封裝在計算屬性的定義里面。
舉個栗子:
計算折扣
<div id="app">
請輸入商品價格: <input type="number" v-model="price"><br>
請輸入商品折扣:
<select v-model="discount" style="width: 100%;">
<option v-for="item, index in discounts" :value="item.value">{{item.label}}</option>
</select><br>
<span style="color: #f00;">成交價:{{price * discount}} 元</span>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
price: 0,
discount: 0.9,
discounts: [{
value: '0.9',
label: '9折'
}, {
value: '0.8',
label: '8折'
}, {
value: '0.7',
label: '7折'
}, {
value: '0.65',
label: '65折'
}]
}
});
</script>
但是,如果計算過程非常復雜該怎么辦呢?
如果當天是11月11號,那么就需要搞促銷,規定商品滿200元減50元,滿400元減150元。這樣一來,要想再全部寫在一個雙大括號里面就顯得有些困難了。
這個時候,我們可以使用Vue提供的計算屬性完成這個任務。
計算屬性也是Vue對象里面專有的一個模塊, 它和methods、data是平級的。
computed: {
payment: function(){
var today = new Date();
// 先獲取今天的日期
var month = today.getMonth() + 1;
var day = today.getDate();
// 模擬今天是 11.11
month = day = 11;
// 是否為雙十一,獲得返利金額
var rebate = 0;
if(month == 11 && day ==11){
if(this.price >= 400){
rebate = 150;
}else if (this.price >= 200){
rebate = 50;
}
}
// 最終的金額
return this.price * this.discount - rebate;
}
}
payment是計算屬性的名稱,頁面上對應的地方也要改過來。
<span style="color: #f00;">成交價:{{payment}} 元</span>
監聽屬性watch
Vue可以監聽屬性的變化,屬性既可以是data里面定義的數據變量,也可以是自己的props模塊中定義的數據。
業務場景為:有一個進度條,它的旁邊有一個“增加進度”按鈕,當進度達到不同的百分比時,就在進度條上方顯示不同的提示。
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="./js/coolBtn.js"></script>
<style>
.progress {
display: inline-block;
background-color: red;
width: 0px;
height: 18px;
}
.primary{
background: #400eff;;
}
.danger{
background: #f56c6c;;
}
.success{
background: #67c23a;;
}
.warning{
background: #9e9a95;;
}
</style>
<div id="app">
<h2>{{msg}}({{progressNum}} %)</h2>
<span class="progress" :style="spanStyle"></span>
<br>
<cool-btn name="增加進度":type="'warning'" @btn-click="addProgress"></cool-btn>
</div>
var app = new Vue({
el: '#app',
data: {
msg: "加油,我看好你哦!",
progressNum: 0,
spanStyle: {}
},
methods: {
// 這個函數 實際上只是希望改變進度的數值,
// 至於進度條的顏色和提示怎樣變化,並不應該交給addProgress處理。
addProgress: function() {
this.progressNum += 10;
if (this.progressNum >= 100) {
this.progressNum = 100;
this.msg = '大功告成,辛苦了!';
return;
}
var background = 'red';
if (this.progressNum >= 80) {
background = 'green';
this.msg = '就差一點點了!';
} else if (this.progressNum >= 50) {
background = 'orange';
this.msg = '有改善咯!';
}
this.spanStyle = {
width: this.progressNum + 'px',
background: background
}
}
}
});
這種做法存在一個問題,那就是addProgress方法顯得太冗余了。
在加載進度條的過程中,我們可以通過點擊按鈕不斷改變進度,但我們實際上只是希望改變進度的數值,至於進度條的顏色和提示怎樣變化,並不應該交給addProgress處理。
我們應該讓addProgress方法保持干凈整潔,至於那些錦上添花的代碼,我們可以將其放到監聽屬性里面,
var app = new Vue({
el: '#app',
data: {
msg: "加油,我看好你哦!",
progressNum: 0,
spanStyle: {}
},
watch: {
progressNum: function(val) {
console.log('我在監聽 progressNum 屬性,現在它的值為:' + val);
if (this.progressNum >= 100) {
this.progressNum = 100;
this.msg = '大功告成,辛苦了!';
return;
}
var background = 'red';
if (this.progressNum >= 80) {
background = 'green';
this.msg = '就差一點點了!';
} else if (this.progressNum >= 50) {
background = 'orange';
this.msg = '有改善咯!';
}
this.spanStyle = {
width: this.progressNum + 'px',
background: background
}
}
},
methods: {
// 這個函數 實際上只是希望改變進度的數值,
// 至於進度條的顏色和提示怎樣變化,並不應該交給addProgress處理。
addProgress: function() {
this.progressNum += 10;
}
}
});
Vue有一個監聽模塊是和data、methods平級的。progressNum是需要被監聽的屬性,賦值一個函數,參數val為當前監聽到的新值。
過濾器 filters
在很多時候,我們拿到的數據在展示到頁面上之前還需要經過一些特殊的處理。
比如,我們希望一個單詞的首字母為大寫,如果頁面上有多個地方都有這樣的需求,我們就得在每一個地方都進行數據處理,或者編寫一個格式化的函數,在每個地方都調用一遍。
但是,這種有針對性的數據處理,不應該使用這種偏業務的解決方式,而應該設計一個過濾器,以專門應對這些情況。
我們用純JavaScript的方式新建一個日期對象,我們希望得到的日期格式為yyyy-mm-dd,可以使用過濾器實現,
<div id="app">
今天是:{{today}}
</div>
var app = new Vue({
el: '#app',
data: {
today: new Date(),
}
});
頁面顯示
今天是:Mon Nov 29 2021 22:09:16 GMT+0800 (中國標准時間)
過濾器就是在Vue對象中添加一個filters模塊,
<script>
var app = new Vue({
el: '#app',
data: {
today: new Date(),
},
filters:{
dateFormat: function(val){
return val.getFullYear() + '-' + (val.getMonth()+1) + '-' + val.getDate();
}
}
});
</script>
dateFormat其實就是一個函數,不過因為它被寫在filters模塊里面, 所以也稱之為過濾器。
過濾器定義好了以后,再回到頁面,哪個數據需要這個過濾器進行過濾,就在豎線后面加上該數據。
<div id="app">
今天是:{{today|dateFormat}}
</div>
過濾器還可以疊加使用,比如希望給日期添加一些樣式。
boxStyle: function(val){
return '<span style="display: inline-block;padding: 6px 10px;background: pink;">'+ val +'</span>'
}
因為這個過濾器添加了CSS樣式,所以在引用的地方就不能用雙大括號了,而應該用v-html進行綁定。
雙大括號是不會解析HTML文檔內容的,不管你輸入什么,都會被當作文本處理,顯示的就是text文本。
<div id="app">今天是:{{today|dateFormat|boxStyle}}</div>
頁面顯示
今天是:<span style="display: inline-block;padding: 6px 10px;background: pink;">2021-11-29</span>
下面的寫法已經廢棄!
Vue從2.0版本開始就不再支持在v-html中使用過濾器了。
<div id="app">
<span v-html="today|dateFormat|boxStyle"></span>
</div>
解決方法是把過濾器當成一個普通方法進行調用。在定義的Vue對象中,所有過濾器都會被掛在$options.filters對象上,
$options.filters
<span v-html="$options.filters.boxStyle($options.filters.dateFormat(today))"></span>
不過,像這種用過濾器的方法添加樣式的情況畢竟少,所以一般用雙大括號就行了。
全局過濾器
所謂的全局過濾器,就是直接綁定在全局Vue對象上的過濾器,任何頁面只要引入了它,就都可以使用了。
注意:定義全局過濾器或全局組件一定要寫在 創建Vue對象 的前面!
<script>
// 全局過濾器
Vue.filter('dateFormat', function(val){
return val.getFullYear() + '-' + (val.getMonth()+1) + '-' + val.getDate();
});
Vue.filter('boxStyle', function(val){
return '<span style="display: inline-block;padding: 6px 10px;background: pink;">'+ val +'</span>'
});
var app = new Vue({
el: '#app',
data: {
today: new Date(),
}
});
</script>
Vue-cli
先將 Node.js安裝好
Node.js使得JavaScript腳本也可以編寫后台程序代碼,不 過,你想要學習Vue-cli,也無須知道那么多,你只要學會使用npm和webpack就行了
npm發布模塊
定義好的組件別人想用怎么辦?
所有人弄一個公共的倉庫,再把這個組件放到倉庫里面,誰想要用,就去倉庫里面取便是了。
npm就是起到了這個作用,在npm倉庫中的都叫作模塊。
所有模塊都發布在www.npmjs.com
在發布之前,需要到npm.js上注冊一個賬號,才有權限發布自己定義的模塊。
新建一個文件夾 :npm- study
在這個文件夾中新建一個yeXiaoFan.js
function hello(){
alert("大家好啊,我是葉小凡!");
}
console.log('yeXiaoFan.js successfully loaded!');
exports.hello = hello;
exports的意思是導出,我們把hello這個函數作為導出模塊的一部分。
接下來,再生成一個文件— package.json
package.json就是這個組件的打包信息。
直接使用命令 npm init
就可以生成這個打包配置文件。
生成package.json的時候,需要輸入一些默認值, name默認是文件夾的名字,我們這次將它改成 huyiqun 吧,其他幾項都默認就好。
package.json
{
"name": "huyiqun", // 模塊名字
"version": "1.0.0", //版本號
"description": "", //模塊的描述
"main": "yeXiaoFan.js", //模塊的啟動文件
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
}, //npm允許在package.json文件里面使用scripts字段定義腳本命令。
"author": "",
"license": "ISC"
}
一般一個模塊發布后,都會在scripts這邊寫上幾個腳本。比如運行整個項目,我們會這樣寫。
"dev": "node yeXiaoFan"
使用 npm run dev
運行,意思就是直接運行這個入口文件
npm run dev表示讓計算機運行dev命令,最終運行的腳本是node yeXiaoFan。
接下來就是發布這個模塊,
用npm adduser 命令添加我們剛才注冊的用戶。
用戶添加好了,接下來就可以發布模塊了,
命令是npm publish
發布完成后,你可以在npm網站上搜索huyiqun,應該就能看到了。
npm安裝模塊
剛才我們成功發布了huyiqun模塊,現在再來看看如何引入這個模塊。
新建一個文件夾:mynpm
在這個目錄執行命令 npm install huyiqun
運行結果:
自動生成了一個package-lock.json 和 node_modules文件夾。
在node_modules 文件夾里就能看到我們發布的huyiqun模塊了。
在mynpm文件夾中創建一個main.js作為項目的入口文件
var hyq = require("huyiqun");
hyq.hello();
require是導入模塊的命令,簡單理解為函數調用即可,參數是一個字符串,就是需要引入的模塊名字。
運行一下,輸入 node main.js
報錯了,提示alert沒有被定義,這是因為現在啟動js腳本直接使用了node命令,而不是在瀏覽器里面運行,自然是沒有alert函數的。
這就需要單獨搭建一個服務了,比如使用Vue-cli搭建一個服務。
注意:npm一般運行速度比較慢,實際開發中應使用國內的服務器地址,使用cnpm下載模塊。安裝cnpm的方法非常簡單,只需要執行以下命令就可以了。
npm install -g cnpm --registry=https://registry.npm.taobao.org
安裝后,下次啟動時就不是npm install了,而是cnpm install。這點區別使速度快了不止一個檔次。
Vue-cli搭建項目
簡單來說,Vue-cli就是進行Vue組件化開發的一個腳手架。
啥是腳手架啊?
腳手架,你就理解為一個項目模板吧。一個前端項目, 肯定會有JS、HTML、CSS文件的。JS文件放哪里好呢,你可能會新建一個叫作js的文件夾,里面放的都是js文件。再比如,針對圖片資源, 你可以新建一個叫pictures的文件夾。
但是,每個人有不同的想法,這種命名約定就不盡相同了。可是實際上,這些資源的歸類確實是必不可少的。腳手架可以幫你生成一個項目模板,在什么文件夾里面放什么資源都是定義好的。這樣就方便開發了,不用總是想着定義資源文件夾的名字。別人拿到你的項目后也方便了不少,因為大家都知道這些文件夾里面放着哪些文件,直接找便是了。並且,針對一些常見的配置,腳手架也會幫你設置好,你只要專心寫業務代碼就可以了。
安裝Vue-cli
創建一個空的Vue-cli文件夾, 然后在這個目錄中運行下面的命令
cnpm install -g @vue/cli@3.0.1
-g的意思就是全局安裝,全局安裝后,在任何其他地方都可以使用Vuecli腳手架。
僅有Vue-cli還不夠,我們還需要安裝它的一個原型工具。
cnpm install -g @vue/cli-service-global@3.0.1
版本號一定要保持一致,不然會出現一些莫名其妙的錯誤。
上面兩個工具都安裝好后,就開始正式創建一個項目吧。
下面采用Vue的方式直接創建。這個項目的名字,就叫作vue-project吧!
vue create vue-project
注意,剛開始會有一些詢問,直接敲回車就行,不要在那傻等着。
項目初始文件
public里面有兩個文件,一個是favicon.ico, 這是項目的圖標。另一個是index.html,這是項目的初始頁面。
這個頁面有一個根節點,是一個div,id為app。
<div id= "app"> </div>
我們的項目可以說全部渲染在這里面。
Public文件夾是默認生成 的,這個文件夾主要用來放置一些公共資源。
接下來是src文件夾,這里面的資源可就多了。
一般而言,項目中的圖片資源、js資源、css資源都沒有統一的定論該放在哪里。腳手架的好處就是告訴你這些靜態資源該放在哪里,如果你是用Vue腳手架生成的項目,那么就是放在assets文件夾里面。
然后是components文件夾,這里存放項目中的一些公共組件,以方便具體的頁面調用。
現在,這里面有一個自動生成的HelloWorld.vue文件,這是一個擴展名為vue的文件。
vue文件是做什么的?
vue文件就是一種容納了js、css和html的文件格式。
看一看HelloWorld.vue里面是什么吧!默認生成的代碼有很多,為了方便起見,我們改寫一下里面的代碼。
<template>
<div class="hello">
<h1>{{ msg }}</h1>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<!-- 添加“scoped”屬性,以限制CSS只用於此組件 -->
<style scoped>
h3 {
margin: 40px 0 0;
}
</style>
template標簽是組件的頁面代碼,script就是寫JavaScript代碼的地方,最后是style標簽,很明顯,這里是寫CSS代碼的地方。
props是定義組件的屬性,有一個屬性是msg,類型是string。
整個文件就是一個組件。接下來,我們看一下App.vue。
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'app',
components: {
HelloWorld }
}
</script>
<style>...省略CSS代碼</style>
import 是引入
App.vue又是一個新的組件,並且引入了HelloWorld.vue,還傳遞了參數。
<HelloWorld msg="Welcome to Your Vue.js App"/>
再看看main.js
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
main.js是項目的入口文件
在代碼的最后,它創建了全局的Vue對象,渲染的目標是‘#app’,也就是剛才看到index.html里面的那個div。
最后三行代碼運用了ES6語法中的箭頭函數,改寫一下就是這樣。
new Vue({
render: function(h){
return h(App)
}
}).$mount('#app')
箭頭函數就是普通函數的一種簡便寫法吧,h是render函數接收的參數,h本身是一個回調函數,所以能夠打括號去執行!
用Vue-cli創建的項目默認支持ES6的語法
啟動一下這個項目吧,啟動的方式已經寫在package.json里面了
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
啟動命令叫作 serve
直接 npm run serve
就可以啟動項目了。
項目默認監聽的是8080號端口。
至此,一個Vue項目完全啟動成功了!
ES6語法
let 和 const
JavaScript是沒有塊級作用域的,這會導致很多問題,比如下面的代碼。
var i = 10;
for(var i=1; i<6; i++){
}
console.log(i); // 6
答案是6,因為JavaScript沒有塊級作用域,這就會導致for循環中定義的i變量覆蓋了全局變量i。
ES6推薦使用let定義變量,這樣就可以實現塊級作用域的效果了,內部的變量不會影響全局。
var i = 10;
for(let i=1; i<6; i++){
};
console.log(i); //10
ES6還可以使用const聲明一個只讀的常量,一旦聲明,該常量的值就不能再被改變,比如下面的代碼。
const NAME1 = '張三';
NAME1 = '李四';
console.log(NAME1);
報錯:
Uncaught TypeError: "NAME1" is read-only
因為常量是只讀的,不允許修改,強行修改只會導致程序報錯。可以說,const的引入提升了JavaScript的安全性。
變量的解構賦值
變量的解構賦值主要分為 數組的解構賦值 和 對象的解構賦值。
一般的用法是,假如我有一個對象, 里面有若干方法:
let Person = {
eat: function(){
console.log("我在吃飯");
},
sleep: function(){
console.log("我在睡覺");
}
}
假如這個對象在其他頁面,或者在其他地方被引用到了,則要想獲取里面的這兩個方法,一般的寫法如下:
let eat = Person.eat;
let sleep = Person.sleep;
// 調用
eat();
sleep();
這是常規的做法
解構賦值的作用是把獲取對象中的方法以及賦值給對應變量的過程一次性做完。
let {eat, sleep} = Person;
一般來說,我們在用let定義變量的時候,是在let的右邊寫一個變量,然后等於某個具體的值。
這種寫法的好處是直接定義了變量的集合,同時程序會智能地在等號右側尋找匹配項。
Person里面的確有eat方法和sleep方法,左側的大括號中也確實存在eat變量和sleep變量,真可謂情投意合。
現在,我們多了一個新的需求,就是獲取Person對象的name屬性。於是,我們這樣寫代碼:
let {eat, sleep, name} = Person;
console.log(name); //undefined
由於Person對象中並沒有name屬性,因此name就沒有對應的匹配項賦值了。顯而易見,name的值打印出來后肯定是undefined。
如果我們不希望name為空,則可以設置一個默認值,將代碼變更一下,就成了這樣。
let {eat, sleep, name="一個神秘的杠精"} = Person;
console.log(name); //一個神秘的杠精
雖然看起來有點怪,但是卻意外地好用,很多項目里面都會這么做。
比如一個修改頁面,我們用Vue將其封裝成一個組件。這個組件在兩種情況下會打開,第一種情況是新增的時候,第二種情況是修改的時候。如果是修改,則會允許組件接收一個data參數,這個data里面包含所有需要修改的信息,只需要讓頁面加載這些信息就行了,然后保存修改即可。如果是新增,則data就是一個空,我們再去渲染它的時候,不就有問題了嗎?所以,這個時候可以用解構賦值中的默認值給data賦一個值。
通過一個簡單的函數說明這種業務場景:
什么參數都沒有傳遞,但是因為在參數這里使用了默認值,於是就采用{name:'jack',sex:'1', salary:'20000'}這個默認對象了。
function initEditInfo(data = {name:'hyq',sex:'1',salary:'20000'}){
let {name, sex, salary} = data;
console.log(name);
console.log(sex);
console.log(salary);
}
initEditInfo();
在實際項目中,解構賦值可以帶來方便,同時解構賦值還有很多其他高級用法,但是筆者不推薦使用過多解構賦值,因為這會導致代碼過於精簡和抽象,不利於后期的維護。如果后面的項目被一個初來乍到的新人接管,則會給他帶來很多困擾。
字符串升級
ES6對傳統字符串進行了一次大規模的改進,主要體現在兩個方面。
第一個改進是允許字符串直接通過for循環的方式遍歷。
注意:這里用的是of,而不是in。如果用in,則獲取的是每個字符對應的下標。
let str= "咱又不是學不會!";
for(let s of str){
console.log(s);
}
另一個改進是允許用反引號進行一些簡化的字符串定義。
模板字符串相當於加強版的字符串,它除了可以作為普通字符串使用,還可以用來定義多行字符串,以及在字符串中加入變量和表達式。
let name = "張三";
let sayHi = `你好啊,${name}一起來happy啊!`;
console.log(sayHi); // 你好啊,張三// 一起來happy啊!
這一改進支持換行和變量注入,這些特性使得JavaScript字符串更加靈活。
除了這兩個改進之外,ES6字符串還提供了一些非常好用的API 方法,如字符串補全。
假設現在有一個需求是依次打印0~99,但是不足2位的數字需要用0左補齊,以往的做法是用if進行判斷,如果小於10, 就在左邊加一個0。
for(let i = 0; i<100; i++){
console.log(i.toString().padStart(2,'0'));
}
padStart:返回新的字符串,表示用參數字符串從頭部(左側)補全原字符串。
padEnd:返回新的字符串,表示用參數字符串從尾部(右側)補全原字符串。
以上兩個方法可以接收兩個參數,第一個參數是指定生成的字符串的最小長度,第二個參數是用來補全的字符串。如果沒有指定第二個參數,則默認用空格填充。
另外,值得一提的是,字符串允許被當作數組一樣使用。換句話說,你可以用下標的方式獲取字符串中某個位置的字符。
Proxy代理
在支持Proxy的瀏覽器環境中,Proxy是一個全局對象,它可以被直接使用。
Proxy(target,handler)是一個構造函數,target是被代理的對象,handlder是聲明了各類代理操作的對象,最終返回一個代理對象。
外界每次通過代理對象訪問target對象的屬性時,就會經過handler對象,
從這個流程來看,代理對象很類似middleware(中間件)。
那么,Proxy可以攔截什么操作呢?
最常見的就是get(讀取)、set(修改)對象屬性等操作。
簡單來說,Proxy的作用就是允許我們聲明一個代理,對某個對象進行一些特殊的訪問攔截。
一個Proxy對象由兩個部分組成:target、handler。
在通過Proxy構造函數生成實例對象時,需要提供這兩個參 數。target即目標對象,handler是一個對象,聲明了代理target的指定行為。
比如,我們現在有如下一個對象:
let obj = {
name: 'hyq',
age: 18
}
當我們希望給obj賦值時,往往會直接這樣做。
obj.age = '28'
這樣不是說不行,但是會出現問題,因為obj的age屬性分明希望得到一個number,但是我們卻賦值了一個string。
我們就希望在給對象賦值的時候限制一下類型。
思路大概是這樣的:給obj生成一個代理,obj就不會直接暴露給外面了。
如果你要操作obj,就和代理說,代理會幫obj做一個簡單的篩選。
let obj = {
name: 'hyq',
age: 18
};
let objProxy = new Proxy(obj, {
// 對象的屬性值被 修改 了會進入set
set(target, key, value){
if(key == "age" && typeof(value) != 'number'){
throw new Error(`修改 ${key} 為 "${value}": 不是數字類型!`);
}
return target[key] = value;
},
// 對象的屬性 被獲取 了會進入get
get(target, key, receiver){
console.log(`當前代理的對象為:${target},某個地方正在獲取對象的 ${key} 屬性, `);
return receiver;
}
});
objProxy.age = '22'; //Uncaught Error: 修改 age 為 "22": 不是數字類型!
objProxy.age = 22;
console.log(objProxy.name);
console.log(obj);
get(target, key, receiver)中的receiver是返回代理對象,而不是返回obj對象。
強化后的數組
ES6對諸多數據類型都進行了強化,自然不可能少得了數組。下面對一些常用的方法進行詳細介紹。
快速構建新數組
Array.of 方法可以將參數中的所有值作為元素而形成數組,參數值可以是不同類型,如果參數為空時,則返回空數組。
let arr = Array.of(11,'22');
console.log(arr); // [11, '22']
Array.from方法
這個方法可以將類數組對象或可迭代對象轉化為數組。
類數組對象就是一種可以遍歷的對象,只要對象有length屬性,而且有諸如0、1、2、3這樣的屬性,那么它就可以被稱為類數組。
下面的對象就可以稱為類數組
let listDate = {
0:'keke',
1:'jerry',
length:2
}
但是它畢竟不是數組,不方便進行某些操作,如push。我們可以用from方法將它轉換為數組。
let listDate1 = Array.from(listDate);
console.log(listDate1); // ['keke', 'jerry']
console.log(listDate); //{0: 'keke', 1: 'jerry', length: 2}
這樣就是貨真價實的數組啦,from方法還可以接收第二個參數,就是一個map回調函數,用於對每個元素進行處理,放入數組的是處理后的元素。
listDate1 = Array.from(listDate1, function(item){
return item + '---';
});
console.log(listDate1); // ['keke---', 'jerry---']
新的數組方法
find:查找數組中符合條件的元素,若有多個符合條件的元素,則返回第一個元素。
let testArr = [2,5,6,10,20];
let arr2 = testArr.find(function(item){
return item>8;
})
console.log(arr2); // 10
findIndex:查找數組中符合條件的元素索引,若有多個符合條件的元素,則返回第一個元素索引。
let testArr = [2,5,6,10,20];
let arr2 = testArr.findIndex(function(item){
return item>8;
})
console.log(arr2); // 3
fill:將一定范圍索引的數組元素內容填充為單個指定的值。
let testArr = [2,5,6,10,20];
testArr.fill(1);
console.log(testArr); // [1, 1, 1, 1, 1]
array.fill(value, start, end)
參數 | 描述 |
---|---|
value | 必需。填充的值。 |
start | 可選。開始填充位置。 |
end | 可選。停止填充位置 (默認為 array.length) |
let testArr = [2,5,6,10,20];
testArr.fill(1,2,4);
console.log(testArr); // [2, 5, 1, 1, 20]
copyWithin:將一定范圍索引的數組元素修改為此數組另一指定范圍索引的元素。
let testArr = [2,5,6,10,20];
testArr.copyWithin(2);
console.log(testArr); // [2, 5, 2, 5, 6]
array.copyWithin(target, start, end)
參數 | 描述 |
---|---|
target | 必需。復制到指定目標索引位置。 |
start | 可選。元素復制的起始位置。 |
end | 可選。停止復制的索引位置 (默認為 array.length)。如果為負值,表示倒數。 |
let testArr = [2,5,6,10,20,30,40,50];
testArr.copyWithin(2,0,-2)
// 0,-2 代表[2, 5, 6, 10, 20, 30],從下標為2的地方開始替換
console.log(testArr); //[2, 5, 2, 5, 6, 10, 20, 30]
ES6 提供三個新的方法 —— entries(),keys()和values() 。
它們都返回一個遍歷器對象,可以用for…of循環進行遍歷,區別是keys()是對鍵名的遍歷、values()是對鍵值的遍歷,entries()是對鍵值對的遍歷。
entries:遍歷
var fruits = ["Banana", "Orange", "Apple", "Mango"];
var x = fruits.entries();
console.log(x.next().value); [0, 'Banana']
console.log(x.next().value); [1, 'Orange']
var obj ={ name: '小明', age: 20, sex: '男' };
console.log(Object.entries(obj)[0]); ['name', '小明']
console.log(Object.entries(obj)[2]); ['sex', '男']
keys:遍歷鍵名。
values:遍歷鍵值。
let arr = ['a', 'b', 'c']
for (let index of arr.keys()) {
console.log(index);
}
// 0
// 1
// 2
for (let item of arr.values()) {
console.log(item);
}
// 'a'
// 'b'
// 'c'
for (let [index, item] of arr.entries()) {
console.log(index, item);
}
// 0 'a'
// 1 'b'
// 2 'c'
上面是對數組的方法
Object的keys()和values()還有entries()方法
let obj = {
name:"張三",
sex:"男",
age:20
}
for ( let key of Object.keys(obj)){
console.log(key)
}
// name
// sex
// age
for ( let val of Object.values(obj)){
console.log(val)
}
// 張三
// 男
// 20
for ( let val of Object.entries(obj)){
console.log(val)
}
// (2) ["name", "張三"]
// (2) ["sex", "男"]
// (2) ["age", 20]
for ( let [key,val] of Object.entries(obj)){
console.log(key,val)
}
// name 張三
// sex 男
// age 20
數組復制
在以前的傳統項目中,如果要復制一個數組,大多采用slice方法,現在可以用“…”的方式快速復制一個數組。
let listData = ['a', 'b', 'c'];
let listData1 = listData;
let slice_listData = listData.slice(0,2);
let newListData = [...listData];
listData[0] = 1;
console.log(slice_listData); // ['a', 'b']
console.log(newListData); // ['a', 'b', 'c']
console.log(listData1); // [1, 'b', 'c']
強化后的函數
ES6對函數也做了很多強化或者說是簡化,尤其著名的就是箭頭函數了。
以前的常規做法是用關鍵字function定義一個函數,而現在ES6的語法允許省略function關鍵字,直接用一個箭頭聲明一個函數。
let sayhi = (name)=>{
console.log(`你好!${name}`);
}
sayhi('hyq'); //你好!hyq
原先的參數列表保留,還是放在一對圓括號里面,但是如果你只有一個參數,則可以省略圓括號。
let sayhi = name =>{
console.log(`你好!${name}`);
}
sayhi('hyq'); //你好!hyq
如果是沒有參數的函數,又該怎么辦呢?當然,打一個小括號是沒有任何問題的。
let sayhi = () =>{
console.log(`你好!`);
}
sayhi('hyq'); //你好!
但是有些人喜歡直接給出一個下畫線,相當於有一個參數,只是這個參數永遠不會被使用罷了。於是,為了降低這個參數的存在感,就用一個下畫線代替它。
let person = {
getNmae: _=>{
return "hyq";
}
}
console.log(person.getNmae()); //hyq
上面的代碼很好地詮釋了這一點,但是一般也不會這么做,不管有沒有參數,如果這個函數是在某個對象里面的,更推薦直接簡寫成如下形式。
let person = {
getNmae(){
return "hyq";
}
}
console.log(person.getNmae()); //hyq
注意:上面的方式並非箭頭函數,而是普通函數的簡寫。
現在再看下一個問題,person對象里面有一個getName方法,這個方法用於返回對象本身的name屬性,下面用this引用一下。
let person = {
name:'hyq',
getName(){
return this.name;
}
}
console.log(person.getName()); //hyq
這樣寫是沒有問題的,相當於如下形式。
let person = {
name:'hyq',
getName: function(){
return this.name;
}
}
但是,如果換成箭頭函數會怎樣呢?
window.name = 'test';
let person = {
name:'hyq',
getName: _=>{
return this.name;
}
}
console.log(person.getName()); //test
原來,箭頭函數體中的 this對象 是定義函數時的對象,而不是使用函數時的對象。
也就是說,定義getName函數的時候,這個函數並不知道自己在person對象中,所以里面的this依然指向window對象。
而如果用function定義函數,則里面的this會在代碼的實際運行過程中動態綁定,因此指向的就是person對象。
所以,使用箭頭函數時,這一點要尤其注意。 永遠記住,箭頭函數中this的指向是定義時所在的作用域,而不是執行時的作用域。
在剛才的例子中, getName方法中的this就指向定義它的作用域,而不是getName方法的調用者。
可能這么講還是比較抽象,請記住一個小竅門:
只要使用了箭頭函數,就不要管這個函數本身了,在外面尋找最近的大括號,然后看這個大括號處在怎樣的環境中,this就指向它!
那么,距離getName方法最近的大括號就是person對象的大括號, person對象處於全局作用域里面,那么this就指向window對象。現在, 我們把這個例子稍微改一改。
window.name = 'test';
let person = {
name:'hyq',
getName: _=>{
return this.name;
},
sayHi:_=>{
setTimeout(_=>{
console.log(`你好!${this.name}`);
}, 1000);
}
}
person.sayHi(); //你好!test
按照剛才的技巧,距離this最近的是setTimeout的參數,也就是那個回調函數的大括號。
注意:這是一個箭頭函數,還要繼續往上找,於是找到了sayHi方法的大括號。
可是sayHi 方法本身又是一個箭頭函數,於是這次尋找還是不算數,還要繼續往上冒泡,最終又找到了person對象,它是window的,於是this指向 window。
那就干脆只要是用到this的地方通通不用箭頭函數了!
window.name = 'test';
let person = {
name:'hyq',
getName: _=>{
return this.name;
},
sayHi(){
setTimeout(function(){
console.log(`你好!${this.name}`);
}, 1000);
}
}
person.sayHi(); //你好!test
這次沒有用箭頭函數,那么之前尋找this的辦法就是有效的。
this永遠指向函數的調用者,因為這個this是在setTimeout函數里面的,它的調用者還是window,並不是person。
在執行sayHi方法的時候,可以臨時保存一下this的指向,這樣就可以在setTimeout中訪問到person對象了。
但是這樣的寫法很煩瑣,而且看起來很奇怪。
let person = {
name:'hyq',
getName: _=>{
return this.name;
},
sayHi(){
let that = this;
setTimeout(function(){
console.log(`你好!${that.name}`);
}, 1000);
}
}
person.sayHi(); //你好!hyq
結合箭頭函數的特性,我們可以稍加改造。
window.name = 'test';
let person = {
name:'hyq',
getName: _=>{
return this.name;
},
sayHi(){
setTimeout(_=>{
console.log(`你好!${this.name}`);
}, 1000);
}
}
person.sayHi(); //你好!hyq
只要你用了箭頭函數,就不要管這個函數本身了,從外面尋找最近的大括號,於是我們找到了sayHi方法(這一次sayHi方法沒有使用箭頭函數)。
sayHi方法是在person對象里面的,所以這次this不會再往上冒泡了,而是定格在這個大括號中,於是this的指向和sayHi方法一樣,都是person對象,終於可以拿到name了!
最后補充一句,如果函數體僅僅是一個簡單的return語句,那么函數體的大括號和return關鍵字都可以省略。
let test_obj = {
sayHi: name=>`你好!${name}`,
}
console.log(test_obj.sayHi('hyq')); //你好!hyq
更加靈活多變的對象
上面已經講解了對象的解構賦值,下面補充一點,ES6中對象的寫法還是有點不同的。
let name = "道德經";
let obj ={
name: name,
}
console.log(obj); //{name: '道德經'}
name: name怎么看都覺得別扭不是嗎?於是,ES6允許我們將其簡寫成name,只要左右兩邊的名字是一樣的,就可以簡寫。
let obj ={name}
我們知道JavaScript中的對象的屬性值是一個字符串,比如這個name: name其實是'name': name,只是我們一般喜歡省略那個引號。
那么,如果這個屬性名稱是一個變量又該怎么辦?
很明顯,這樣的寫法是錯誤的,key會被當作一個字符串進行處理。
let key = 'name';
let obj = {
key: '杠精'
}
console.log(obj); //{key: '杠精'}
還記得我們是如何調用對象的屬性的嗎?
可以用“.”,也可以 用“[]”,這里也是一樣的,用“[]”就可以解決問題。
let key = 'name';
let obj = {
[key]: '杠精'
}
console.log(obj); //{name: '杠精'}
promise對象和async函數
最簡單的需求:
制作一個定時器,2s過后,獲取一個字符串,然后在控制台輸出這個字符串。
let gift = null;
setTimeout(_=>{
gift = "吸星大法";
},2000);
console.log(`我學會了${gift}`);
代碼中的問題很明顯,這是一個異步操作,需要在2s后才會執行gift變量的賦值語句,所以,還沒有等gift有值,語句就已經輸出了。
一個最容易想到的解決辦法就是把輸出語句放到setTimeout里面,這樣做是絕對正確的,
但是如果異步操作有很多,就會出現層層嵌套的問題。
let gift = null;
setTimeout(_=>{
gift = "吸星大法";
console.log(`我學會了${gift}`);
},2000);
我們也可以使用Proxy代理觀察gift值的變化。
let gift = null;
let obj = {gift}
let objProxy = new Proxy(obj,{
set(target, key, value){
if(key == "gift"){
target[key] = value;
console.log(`我學會了${target[key]}`);
return true;
}
},
get(target, key){
return target[key];
}
});
setTimeout(_=>{
objProxy.gift = "吸星大法";
},2000);
這樣就可以通過觀察者和代理模式監聽gift的變化,從而完成異步監聽的效果。
但是,很明顯,這樣寫代碼太復雜了。
這里,我們可以使用一個新的對象——promise。
promise是異步編程的一種解決方案。從語法上說,promise是一個對象,使用它可以獲取異步操作的消息。
在ES6中,promise被列為正式規范,統一了用法,原生提供了promise對象。
let gift = null;
new Promise((resolve, reject)=>{
setTimeout(_=>{
gift = "吸星大法";
resolve(gift);
},2000);
}).then(gift=>{
console.log(`我學會了${gift}`);
});
promise對象在創建的時候分別接收了2個內部的函數鈎子: resolve(已完成)和reject(已拒絕)。
promise對象就是一種承諾,在必要的時候,它會告知外部本次異步操作已經完成或者拒絕,如果是完成,則觸發后面的then方法;如果是拒絕,則觸發catch方法。
比如,我們把代碼改造為有20%的概率可以獲得吸星大法,有80%的概率什么也得不到,即表示獲取異常(reject)。
let gift = null;
new Promise((resolve, reject)=>{
setTimeout(_=>{
if(Math.random() < 0.2){
gift = "吸星大法";
resolve(gift);
}else{
gift = "個屁";
reject(gift);
}
},2000);
}).then(gift=>{
console.log(`我學會了${gift}`);
}).catch(gift=>{
console.log(`我學會了${gift}`);
});
這便是用promise對象處理異步操作的思路。
看到這里,可能有的人又會納悶,雖然用promise對象處理起來更加優雅,但是我們不是還要在對應的then方法或者catch方法里面進行操作嗎?
能不能直接給我resolve里面的值,不要逼着我去then里面處理數據?
辦法是有的,這就需要配合async函數和await關鍵字了。
let getGiftAsync = _=>{
return new Promise((resolve, reject)=>{
setTimeout(_=>{
if(Math.random() < 0.2){
let gift = "吸星大法";
resolve(gift);
}else{
let gift = "個屁";
reject(gift);
}
},2000);
});
};
async function executeAsyncFunc(){
let gift = await getGiftAsync();
console.log(`我學會了${gift}`);
};
executeAsyncFunc();
之前,代碼的困境是無法脫離then和catch的回調函數,導致代碼還是有些冗余,其實我們只是希望得到resolve里面的參數而已,下面簡單介紹上面的代碼:
getGiftAsync函數返回了一個promise對象,邏輯和剛才一樣,
然后在executeAsyncFunc函數的左邊加上了async,代表這是一個異步處理函數。
只有加上了async關鍵字的函數,內部才可以使用await關鍵字。
async是ES7才提供的與異步操作有關的關鍵字,async函數執行時,如果遇到await就會先暫停執行,等到觸發的異步操作完成后,才會恢復async函數的執行並返回解析值。