day 81 Vue學習一之vue初識
- 2020 年 1 月 16 日
- 筆記
Vue學習一之vue初識
本節目錄
一 vue初識
vue稱為漸進式js框架,這個框架用來做前後端分離的項目,之前我們學習django,知道django是一個MTV模式的web框架,urls–views–templates,模板渲染通過後端的程式碼來實現數據的渲染,再加上前端一些簡單的dom操作來完成網頁的開發,當我們做一個複雜的大型的網頁的時候,你會發現這種模式作起來會比較複雜,擴展起來也比較困難,因為前後端沒有分離開,耦合性太高,牽一髮而動全身,所以人們就開始想,如果能有專門的人來開發前端,專門的人來開發後端,前端頁面就是前端語言來寫,後端服務端程式碼就是後端服務端程式碼來寫,兩者之前只有數據的交流,那麼以後頁面在進行拓展,進行功能的更新的時候就會變得比較簡單,因此vue就誕生了,之前我們前端頁面拿到數據都是通過dom操作或者django的模板語言來進行數據的渲染的,有了前端框架vue,就不需要他們了,並且頻繁的dom操作,創建標籤添加標籤對頁面的性能是有影響的,那麼直接數據驅動視圖,將django的MTV中的T交給vue來寫,也就是那個templates裡面的內容,並且前端的vue拿到了T這部分的工作,MTV前身是MVC,可以將vue拿到的T的工作稱為view視圖,就是完成MVC的V視圖層工作,只不過V稱為視圖函數,重點在函數,而vue我們稱為視圖,接到後端的數據(通過介面url,獲得json數據),直接通過vue的視圖渲染在前端。


前端三大框架,Vue、Angular、React,vue是結合了angular和react的優點開發出來的,是中國人尤雨溪開發的,angular很多公司也在用,是Google開發的,老項目一般是angular2.0,最新的是6.0的,但是它是基於另外一個版本的js,叫做typescript,所以如果將來你工作用的是angular6.0,那麼要自己提前學一下typescript,也比較簡單,react是facebook開發的,其實越大型的項目react越好用,個人觀點昂,react裡面用的多是高階函數,需要你對js特別熟,對初學者不是很友好,但是你越熟練,用起來越nb,將來如果需要,大家再學習吧,爭取哪天給大家整理出來,在githup上react比vue的星還多一些。

前後端分離項目:分工明確
前端做前端的事情:頁面+交互+兼容+封裝+class+優化 (技術棧:vue+vue-router+vuex+axios+element-ui)
後端做後端的事情:介面+表操作+業務邏輯+封裝+class+優化 (技術棧:restframework框架+django框架+mysqlredis等)
畫一個django和vue的對比圖吧

二 ES6基本語法

由於後面學習vue你會發現有很多es6的語法,所以我們先學一些es6的基本語法
1 let聲明變數
1.1 基本用法
ES6 新增了let
命令,用來聲明變數。它的用法類似於var
,但是所聲明的變數,只在let
命令所在的程式碼塊內有效,其實在js裡面{}大括弧括起來的表示一個程式碼塊。

{ let a = 10; var b = 1; //相當於將b的聲明放在了作用域的外面前面,var b;然後這裡只是賦值 } a // ReferenceError: a is not defined. b // 1

上面程式碼在程式碼塊之中,分別用let
和var
聲明了兩個變數。然後在程式碼塊之外調用這兩個變數,結果let
聲明的變數報錯,var
聲明的變數返回了正確的值。這表明,let
聲明的變數只在它所在的程式碼塊有效。
再看一個例子:

<script> var l = []; l[1] = 'aa'; console.log(l[0],l[1]); //undefined "aa" 可以給數組通過索引來賦值,如果你給索引1賦值了,那麼索引0的值為undefined console.log(a); //undefined //因為var存在變數提升的問題,會在這個列印前面先聲明一個var a;然後後面在進行a=1的賦值,所以列印出來不報錯,而是列印的undefined,let不存在這個問題,let只在自己的程式碼塊中生效 { //js裡面大括弧表示一個程式碼塊 var a = 1; let b = 2; } console.log(a); //1 console.log(b); // 報錯 </script>

for
循環的計數器,就很合適使用let
命令。
for (let i = 0; i < 10; i++) { // ... } console.log(i); // ReferenceError: i is not defined
上面程式碼中,計數器i
只在for
循環體內有效,在循環體外引用就會報錯。
下面的程式碼如果使用var
,最後輸出的是10
。

var a = []; for (var i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 10

上面程式碼中,變數i
是var
命令聲明的,在全局範圍內都有效,所以全局只有一個變數i
。每一次循環,變數i
的值都會發生改變,而循環內被賦給數組a
的函數內部的console.log(i)
,裡面的i
指向的就是全局的i
。也就是說,所有數組a
的成員裡面的i
,指向的都是同一個i
,導致運行時輸出的是最後一輪的i
的值,也就是 10。
如果使用let
,聲明的變數僅在塊級作用域內有效,最後輸出的是 6。

var a = []; for (let i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 6

上面程式碼中,變數i
是let
聲明的,當前的i
只在本輪循環有效,所以每一次循環的i
其實都是一個新的變數,所以最後輸出的是6
。你可能會問,如果每一輪循環的變數i
都是重新聲明的,那它怎麼知道上一輪循環的值,從而計算出本輪循環的值?這是因為 JavaScript 引擎內部會記住上一輪循環的值,初始化本輪的變數i
時,就在上一輪循環的基礎上進行計算。
另外,for
循環還有一個特別之處,就是設置循環變數的那部分是一個父作用域,而循環體內部是一個單獨的子作用域。

for (let i = 0; i < 3; i++) { let i = 'abc'; console.log(i); } // abc // abc // abc

上面程式碼正確運行,輸出了 3 次abc
。這表明函數內部的變數i
與循環變數i
不在同一個作用域,有各自單獨的作用域。
1.2 不存在變數提升
var
命令會發生「變數提升」現象,即變數可以在聲明之前使用,值為undefined
。這種現象多多少少是有些奇怪的,按照一般的邏輯,變數應該在聲明語句之後才可以使用。
為了糾正這種現象,let
命令改變了語法行為,它所聲明的變數一定要在聲明後使用,否則報錯。

// var 的情況 console.log(foo); // 輸出undefined var foo = 2; // let 的情況 console.log(bar); // 報錯ReferenceError let bar = 2;

上面程式碼中,變數foo
用var
命令聲明,會發生變數提升,即腳本開始運行時,變數foo
已經存在了,但是沒有值,所以會輸出undefined
。變數bar
用let
命令聲明,不會發生變數提升。這表示在聲明它之前,變數bar
是不存在的,這時如果用到它,就會拋出一個錯誤。
1.3 暫時性死區
只要塊級作用域記憶體在let
命令,它所聲明的變數就「綁定」(binding)這個區域,不再受外部的影響。
var tmp = 123; if (true) { tmp = 'abc'; // ReferenceError let tmp; }
上面程式碼中,存在全局變數tmp
,但是塊級作用域內let
又聲明了一個局部變數tmp
,導致後者綁定這個塊級作用域,所以在let
聲明變數前,對tmp
賦值會報錯。
ES6 明確規定,如果區塊中存在let
和const
命令,這個區塊對這些命令聲明的變數,從一開始就形成了封閉作用域。凡是在聲明之前就使用這些變數,就會報錯。
總之,在程式碼塊內,使用let
命令聲明變數之前,該變數都是不可用的。這在語法上,稱為「暫時性死區」(temporal dead zone,簡稱 TDZ)。暫時性死區的本質就是,只要一進入當前作用域,所要使用的變數就已經存在了,但是不可獲取,只有等到聲明變數的那一行程式碼出現,才可以獲取和使用該變數。
1.4 不允許重複聲明
let
不允許在相同作用域內,重複聲明同一個變數。

// 報錯 function func() { let a = 10; var a = 1; } // 報錯 function func() { let a = 10; let a = 1; }

因此,不能在函數內部重新聲明參數。

function func(arg) { let arg; } func() // 報錯 function func(arg) { { let arg; } } func() // 不報錯

2. 作用域
ES5 只有全局作用域和函數作用域,沒有塊級作用域,這帶來很多不合理的場景。
第一種場景,內層變數可能會覆蓋外層變數。

var tmp = new Date(); function f() { console.log(tmp); if (false) { var tmp = 'hello world'; } } f(); // undefined

上面程式碼的原意是,if
程式碼塊的外部使用外層的tmp
變數,內部使用內層的tmp
變數。但是,函數f
執行後,輸出結果為undefined
,原因在於變數提升,導致內層的tmp
變數覆蓋了外層的tmp
變數。
第二種場景,用來計數的循環變數泄露為全局變數。

var s = 'hello'; for (var i = 0; i < s.length; i++) { console.log(s[i]); } console.log(i); // 5

上面程式碼中,變數i
只用來控制循環,但是循環結束後,它並沒有消失,泄露成了全局變數。
ES6中的作用域:
let
實際上為 JavaScript 新增了塊級作用域。

function f1() { let n = 5; if (true) { let n = 10; } console.log(n); // 5 }

上面的函數有兩個程式碼塊,都聲明了變數n
,運行後輸出 5。這表示外層程式碼塊不受內層程式碼塊的影響。如果兩次都使用var
定義變數n
,最後輸出的值才是 10。
ES6 允許塊級作用域的任意嵌套。
{{{{{let insane = 'Hello World'}}}}};
上面程式碼使用了一個五層的塊級作用域。外層作用域無法讀取內層作用域的變數。
{{{{ {let insane = 'Hello World'} console.log(insane); // 報錯 }}}};
內層作用域可以定義外層作用域的同名變數。
{{{{ let insane = 'Hello World'; {let insane = 'Hello World'} }}}};
3.const聲明常量
3.1 基本用法
const
聲明一個只讀的常量。一旦聲明,常量的值就不能改變。
const PI = 3.1415; PI // 3.1415 PI = 3; // TypeError: Assignment to constant variable.
上面程式碼表明改變常量的值會報錯。
const
聲明的變數不得改變值,這意味著,const
一旦聲明變數,就必須立即初始化,不能留到以後賦值。
const foo; // SyntaxError: Missing initializer in const declaration
上面程式碼表示,對於const
來說,只聲明不賦值,就會報錯。
const
的作用域與let
命令相同:只在聲明所在的塊級作用域內有效。
if (true) { const MAX = 5; } MAX // Uncaught ReferenceError: MAX is not defined
const
命令聲明的常量也是不提升,同樣存在暫時性死區,只能在聲明的位置後面使用。
if (true) { console.log(MAX); // ReferenceError const MAX = 5; }
上面程式碼在常量MAX
聲明之前就調用,結果報錯。
const
聲明的常量,也與let
一樣不可重複聲明。
var message = "Hello!"; let age = 25; // 以下兩行都會報錯 const message = "Goodbye!"; const age = 30;
3.2 本質
const
實際上保證的,並不是變數的值不得改動,而是變數指向的那個記憶體地址所保存的數據不得改動。對於簡單類型的數據(數值、字元串、布爾值),值就保存在變數指向的那個記憶體地址,因此等同於常量。但對於複合類型的數據(主要是對象和數組),變數指向的記憶體地址,保存的只是一個指向實際數據的指針,const
只能保證這個指針是固定的(即總是指向另一個固定的地址),至於它指向的數據結構是不是可變的,就完全不能控制了。因此,將一個對象聲明為常量必須非常小心。

const foo = {}; // 為 foo 添加一個屬性,可以成功 foo.prop = 123; foo.prop // 123 // 將 foo 指向另一個對象,就會報錯 foo = {}; // TypeError: "foo" is read-only

上面程式碼中,常量foo
儲存的是一個地址,這個地址指向一個對象。不可變的只是這個地址,即不能把foo
指向另一個地址,但對象本身是可變的,所以依然可以為其添加新屬性。
下面是另一個例子。
const a = []; a.push('Hello'); // 可執行 a.length = 0; // 可執行 a = ['Dave']; // 報錯
上面程式碼中,常量a
是一個數組,這個數組本身是可寫的,但是如果將另一個數組賦值給a
,就會報錯。
ES6 聲明變數的六種方法
ES5 只有兩種聲明變數的方法:var
命令和function
命令。ES6 除了添加let
和const
命令,另外兩種聲明變數的方法:import
命令和class
命令。所以,ES6 一共有 6 種聲明變數的方法。
4.模板字元串
模板字元串就是兩個反引號,也就是tab鍵上面的那個鍵,括起來的字元串,就是模板字元串,var或者let聲明的變數都可以被模板字元串直接通過${變數名}來使用,看例子

var aa = 'chao'; let bb = 'jj'; var ss = `你好${aa}`; ss "你好chao" var ss2 = `你好${bb}`; ss2 "你好jj" let ss3 = `你好${aa}`; ss3 "你好chao"

總結:
let :特點: 1.a是局部作用域的 2.不存在變數提升 3.不能重複聲明(var可以重複聲明),
const :特點: 1.局部作用域 2.不存在變數提升 3.不能重複聲明 4.一般聲明不可變的量
模板字元串:tab鍵上面的反引號,${變數名}來插入值
5.函數
說到函數,我們來看看es5和es6是怎麼聲明函數的

//ES5寫法 function add(x){ return x } add(5); //匿名函數 var add = function (x) { return x }; //ES6的匿名函數 let add = function (x) { return x }; add(5); //ES6的箭頭函數,就是上面方法的簡寫形式 let add = (x) => { return x }; console.log(add(20)); //更簡單的寫法,但不是很易閱讀 let add = x => x; console.log(add(5)); 多個參數的時候必須加括弧,函數返回值還是只能有一個,沒有參數的,必須寫一個() let add = (x,y) => x+y;

下面來學一下自定義對象中封裝函數的寫法,看例子:

//es5對象中封裝函數的方法 var person1 = { name:'超', age:18, f1:function () { //在自定義的對象中放函數的方法 console.log(this);//this指向的是當前的對象,{name: "超", age: 18, f1: ƒ} console.log(this.name) // '超' } }; person1.f1(); //通過自定對象來使用函數 //ES6中自定義對象中來封裝箭頭函數的寫法 let person2 = { name:'超', age:18, f1: () => { //在自定義的對象中放函數的方法 console.log(this); //this指向的不再是當前的對象了,而是指向了person的父級對象(稱為上下文),而此時的父級對象是我們的window對象,Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …} console.log(window);//還記得window對象嗎,全局瀏覽器對象,列印結果和上面一樣:Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …} console.log(this.name) //啥也列印不出來 } }; person2.f1(); //通過自定對象來使用函數 //而我們使用this的時候,希望this是person對象,而不是window對象,所以還有下面這種寫法 let person3 = { name:'超', age:18, f1(){ //相當於f1:function(){},只是一種簡寫方式,稱為對象的單體模式寫法,寫起來也簡單,vue裡面會看用到這種方法 console.log(this);//this指向的是當前的對象,{name: "超", age: 18, f1: ƒ} console.log(this.name) //'超' } }; person3.f1()

6.類
我們看看es5和es6的類寫法對比

<script> //es5寫類的方式 function Person(name,age) { //封裝屬性 this.name = name; this.age = age; } //封裝方法,原型鏈 Person.prototype.f1 = function () { console.log(this.name);//this指的是Person對象, 結果:'超' }; //封裝方法,箭頭函數的形式寫匿名函數 Person.prototype.f2 = ()=>{ console.log(this); //Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …} this指向的是window對象 }; var p1 = new Person('超',18); p1.f1(); p1.f2(); //其實在es5我們將js的基本語法的時候,沒有將類的繼承,但是也是可以繼承的,還記得嗎,那麼你想,繼承之後,我們是不是可以通過子類實例化的對象調用父類的方法啊,當然是可以的,知道一下就行了,我們下面來看看es6裡面的類怎麼寫 class Person2{ constructor(name,age){ //對象裡面的單體模式,記得上面將函數的時候的單體模式嗎,這個方法類似於python的__init__()構造方法,寫參數的時候也可以寫關鍵字參數 constructor(name='超2',age=18) //封裝屬性 this.name = name; this.age = age; } //注意這裡不能寫逗號 showname(){ //封裝方法 console.log(this.name); } //不能寫逗號 showage(){ console.log(this.age); } } let p2 = new Person2('超2',18); p2.showname() //調用方法 '超2' //es6的類也是可以繼承的,這裡咱們就不做細講了,將來你需要的時候,就去學一下吧,哈哈,我記得是用的extends和super </script>

三 Vue的基本用法
1.下載安裝
我們使用vue就要把人家下載下來安裝一下,就像你使用django框架一樣,需要下載安裝django才能使用,這個vue框架小而精,但是功能很強大,之前我們的js或者jQuery操作基本都可以通過vue來完成,我們下面是按照vue2.x學的,如果vue更新為3.0了,那麼大家記得去學學裡面的新語法。
方式1:
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
方式2:

引用:
<script src="vue.js"></script>
下載的三種方式:cdn,下載js npm下載(後面再說這個),最好的引用方式是先cdn引入然後備選本地js文件引入,減輕自己伺服器壓力,這些你們先作為了解。
引用了vue之後,我們直接打開引用了vue的這個html文件,然後在瀏覽器調試窗口輸入Vue,你會發現它就是一個構造函數,也就是咱們js裡面實例化一個類時的寫法:

2 vue的模板語法
我的文件的目錄結構是這樣的:

簡單看一個模板語法的例子:就是上面這個html文件

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>test vue</title> </head> <body> <div id="app"> <!-- vue的模板語法,和django的模板語法類似 --> <h2>{{ msg }}</h2> <!-- 放的是變數,就會去下面的Vue對象中的data屬性中的鍵去找對應的數據,注意語法規範,中間的數據前後都有一個空格 --> <h2>{{ 'xxxxx' }}</h2> <!-- 寫個字元串就直接顯示這個字元串 --> </div> <div id="content">{{ msg }}</div> <!--不生效--> <!-- 1.引包 --> <script src="vue.js"></script> <script> //2.實例化對象 new Vue({ //實例化的時候要傳一個參數,options配置選項,是一個自定義對象,下面就看看這些配置選項都是什麼,是我們必須要知道的東西,el和data是必須要寫的 el:'#app', //el是當前我們實例化對象綁定的根元素(標籤),會到html文檔中找到這個id屬性為app的標籤,在html裡面寫一個id屬性為app的div標籤,意思就是說,我現在實例化的這個Vue對象和上面這個id為app的div綁定到了一起,在這個div裡面使用vue的語法才能生效,就像一個地主圈了一塊地一樣,那麼接下來就要種東西了 data:{ //data是數據屬性,就是我們要在地裡面種的東西了,是一個自定義對象 msg:'黃瓜', //這些數據以後是通過資料庫裡面調出來,然後通過後端程式碼的介面接收到的,現在寫死了,先看看效果,我們在上面的div標籤裡面寫一個其他的標籤,然後寫上{{ msg }},這樣就相當於我們在id為app的div標籤的這個地裡面種了一個種子,這個種子產的是黃瓜,那麼我們打開頁面就直接看到黃瓜了 } }) </script> </body> </html>

看頁面效果:

其他的模板語法的使用:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>test vue</title> </head> <body> <div id="app"> <!-- vue的模板語法,和django的模板語法類似 --> <h2>{{ msg }}</h2> <!-- 放一個變數,會到data屬性中去找對應的值 --> <!-- 有人說,我們直接這樣寫數據不就行嗎,但是你注意,我們將來的數據都是從後端動態取出來的,不能寫死這些數據啊,你說對不對 --> <h2>{{ 'hello beautiful girl!' }}</h2> <!-- 直接放一個字元串 --> <h2>{{ 1+1 }}</h2> <!-- 四則運算 --> <h2>{{ {'name':'chao'} }}</h2> <!-- 直接放一個自定義對象 --> <h2>{{ person.name }}</h2> <!-- 下面data屬性裡面的person屬性中的name屬性的值 --> <h2>{{ 1>2?'真的':'假的' }}</h2> <!-- js的三元運算 --> <h2>{{ msg2.split('').reverse().join('') }}</h2> <!-- 字元串反轉 --> </div> <!-- 1.引包 --> <script src="vue.js"></script> <script> //2.實例化對象 new Vue({ el:'#app', data:{ msg:'黃瓜', person:{ name:'超', }, msg2:'hello Vue' } }) </script> </body> </html>

看效果:

3 vue的指令系統
vue裡面所有的指令系統都是v開頭的,v-text和v-html(重點是v-html),使用指令系統就能夠立馬幫你做dom操作了,不需要咱們自己再寫dom操作了,所以我們之後就學習vue的指令系統語法就可以了。
3.1 v-text和v-html
v-text相當於innerText,相當於我們上面說的模板語法,直接在html中插值了,插的就是文本,如果data裡面寫了個標籤,那麼通過模板語法渲染的是文本內容,這個大家不陌生,這個v-text就是輔助我們使用模板語法的
v-html相當於innerHtml
模板語法data中寫個標籤的例子:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>test vue</title> </head> <body> <div id="app"> <!-- vue的模板語法 --> <div>{{ text }}</div> </div> <!-- 1.引包 --> <script src="vue.js"></script> <script> //2.實例化對象 new Vue({ el:'#app', data:{ text:'<h2>只要vue學得好</h2>' //這裡放個標籤 } }) </script> </body> </html>

看效果:

上面我們使用data屬性的時候,都是用的data:{}對應一個自定義對象,但是在我們後學的學習中,這個data我們一般都是對應一個函數,並且這個函數裡面必須return {}一個對象,看例子:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>test vue</title> </head> <body> <div id="app"> <!-- vue的模板語法 --> <div>{{ msg }}</div> </div> <hr> <div id="content"> {{ msg }} </div> <script src="vue.js"></script> <script> //每一個Vue對象都可以綁定一個根元素,比如上面我們有兩個div標籤,一個id為app一個id為content,我們就可以寫兩個Vue對象,意思是告訴大家是可以綁定多個根元素的,使用多個Vue對象就可以了,但是一般我們一個就夠用了,充當body的角色,那麼說在body標籤給個id為app行不行啊,當然行,但是一般不這麼干 new Vue({ el:'#content', data:function () { return{ msg:'哈哈' } } }); //所以重點看下面這個就行了 new Vue({ el:'#app', // data:function () { //大家記住一點,將來凡是涉及到data數據屬性的,牽扯到組件的概念的時候,data都對應一個函數,寫法就是這樣的,因為後面我們要學習的組件中明確規定,組件中的data必須對應函數,所以大家就用函數來寫data。 // // return{ //函數裡面必須return一個對象{} // msg:'超', //這個return的對象裡面再玩我們的數據 // } // } //我們對於這個函數就可以簡寫,按照單體模式的寫法,大家還記得單體模式嗎 data(){ //單體模式 return{ msg:'超', } } }) </script> </body> </html>

下面看一看v-text和v-html:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>test vue</title> </head> <body> <div id="app"> <!-- vue的模板語法 --> <div>{{ msg }}</div> <div v-text="msg"></div> <div v-html="msg"></div> </div> <script src="vue.js"></script> <script> new Vue({ el:'#app', data(){ //記著data中是一個函數,函數中return一個對象,可以是一個空對象,但必須return return{ msg:'<h2>超</h2>', //後端返回的是標籤,那麼我們就可以直接通過v-html渲染出來標籤效果 } } }) </script> </body> </html>

看頁面效果:

3.2 v-if和v-show
在模板語法裡面{{ 屬性或者函數 }},雙大括弧裡面可以是data裡面的屬性,也可以是一個函數,看寫法:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>test vue</title> </head> <body> <div id="content"> <p>{{ msg }}</p> <p>{{ add(5,6) }}</p> <!-- 使用函數,如果函數沒有返回值,這裡啥也不顯示--> </div> <script src="vue.js"></script> <script> new Vue({ el:'#content', data(){ //記著data中是一個函數,函數中return一個對象,可以是一個空對象,但必須return return{ msg:'<h2>超</h2>', //後端返回的是標籤,那麼我們就可以直接通過v-html渲染出來標籤效果 } }, methods:{ //在模板語法中使用函數的時候,不是在data屬性裡面寫了,而是在這個methods裡面寫,看寫法 add(x,y){ return x+y; } } }) </script> </body> </html>

然後我們接著來看一下v-if和v-show以及v-on的簡單用法
v-show的例子:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>test vue</title> <style> .box{ height: 200px; width: 200px; background-color: red; } </style> </head> <body> <div id="content"> <p>{{ msg }}</p> <p>{{ add(5,6) }}</p> <!-- 使用函數,如果函數沒有返回值,這裡啥也不顯示--> <!-- 注意,使用指令系統的時候,v-xxx=字元串,必須是個字元串,而且這個字元串必須是Vue對象裡面聲明的屬性或者方法,不然在瀏覽器上會報錯,而且使用模板語法{{}}的時候,只能寫在標籤的裡面 --> <div class="box" v-show="isShow"></div> <!-- 根據isShow的值來顯示或者隱藏標籤 --> </div> <script src="vue.js"></script> <script> new Vue({ el:'#content', data(){ return{ msg:'<h2>超</h2>', // isShow:true, //true顯示標籤,false隱藏標籤,就是給標籤加一個css屬性為display:'none' // isShow:1===1, isShow:1===2, } }, methods:{ add(x,y){ return x+y; } } }) </script> </body> </html>

v-show結合v-on的例子2:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>test vue</title> <style> .box{ height: 200px; width: 200px; background-color: red; } </style> </head> <body> <div id="content"> <p>{{ msg }}</p> <p>{{ add(5,6) }}</p> <!-- 使用函數,如果函數沒有返回值,這裡啥也不顯示--> <!-- 注意,使用指令系統的時候,v-xxx=字元串,必須是個字元串,而且這個字元串必須是Vue對象裡面聲明的屬性或者方法,不然在瀏覽器上會報錯,而且使用模板語法{{}}的時候,只能寫在標籤的裡面 --> <div class="box" v-show="isShow"></div> <!-- 根據isShow的值來顯示或者隱藏標籤 --> <div> <!-- 點擊隱藏按鈕讓上面的class值為box的標籤隱藏或者顯示,之前我們通過dom操作來完成事件驅動的,這裡我們通過vue的數據驅動來搞 --> <!--<button v-on:click="函數名,找Vue對象中methods裡面的函數">隱藏或者顯示</button>,通過v-on指令來實現事件的綁定和驅動--> <button v-on:click="handlerClick">隱藏或者顯示</button> </div> </div> <script src="vue.js"></script> <script> new Vue({ el:'#content', data(){ return{ msg:'<h2>超</h2>', isShow:true, } }, methods:{ add(x,y){ return x+y; }, handlerClick(){ //數據驅動來控制標籤的顯示隱藏 // console.log(this) //this是當前Vue對象,當我們列印這個對象的時候,在瀏覽器控制台你會發現Vue對象自帶的屬性(eldatamethods等,會在屬性名前面加一個$符號,以便和我們自定義的屬性(msgisShowadd等等)區分) this.isShow = !this.isShow; //更改isShow的數據,你會發現數據一邊,上面的標籤就根據數據來顯示或者隱藏,這就是vue的思想,數據驅動,完全不需要我們自己寫dom操作來完成標籤的顯示或者隱藏了 } } }) </script> </body> </html>

效果:

接下來看一下v-show和v-if的區別

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>test vue</title> <style> .box{ height: 200px; width: 200px; background-color: red; } .box2{ height: 200px; width: 200px; background-color: green; } </style> </head> <body> <div id="content"> <p>{{ msg }}</p> <p>{{ add(5,6) }}</p> <div class="box" v-show="isShow"></div> <!-- v-if也是通過值來判斷顯示或者隱藏,但是這個隱藏不是加diaplay屬性為none了,而是直接將這個標籤刪除了,通過瀏覽器控制台你會發現,一隱藏,下面這個標籤就沒有了,一顯示,這個標籤又重新添加回來了,這就是v-if和v-show的區別 --> <div class="box2" v-if="isShow"></div> <div> <button v-on:click="handlerClick">隱藏或者顯示</button> </div> </div> <script src="vue.js"></script> <script> new Vue({ el:'#content', data(){ return{ msg:'<h2>超</h2>', isShow:true, } }, methods:{ add(x,y){ return x+y; }, handlerClick(){ //數據驅動來控制標籤的顯示隱藏 // console.log(this) //this是當前Vue對象,當我們列印這個對象的時候,在瀏覽器控制台你會發現Vue對象自帶的屬性(eldatamethods等,會在屬性名前面加一個$符號,以便和我們自定義的屬性(msgisShowadd等等)區分) this.isShow = !this.isShow; //更改isShow的數據,你會發現數據一邊,上面的標籤就根據數據來顯示或者隱藏,這就是vue的思想,數據驅動,完全不需要我們自己寫dom操作來完成標籤的顯示或者隱藏了 } } }) </script> </body> </html>

看效果,v-if是標籤的添加和刪除,v-show是標籤的顯示和隱藏,v-if的渲染效率開銷比較大,v-if叫做條件渲染,還有個v-else,一會我們測試一下。

v-if和v-show的區別,官網解釋:

v-if 是「真正」的條件渲染,因為它會確保在切換過程中條件塊內的事件監聽器和子組件適當地被銷毀和重建。 v-if 也是惰性的:如果在初始渲染時條件為假,則什麼也不做——直到條件第一次變為真時,才會開始渲染條件塊。 相比之下,v-show 就簡單得多——不管初始條件是什麼,元素總是會被渲染,並且只是簡單地基於 CSS 進行切換。 一般來說,v-if 有更高的切換開銷,而 v-show 有更高的初始渲染開銷。因此,如果需要非常頻繁地切換,則使用 v-show 較好;如果在運行時條件很少改變,則使用 v-if 較好。

接下來我們簡單看一下v-if和v-else

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>test vue</title> <style> .box{ height: 200px; width: 200px; background-color: red; } .box2{ height: 200px; width: 200px; background-color: green; } </style> </head> <body> <div id="content"> <p>{{ msg }}</p> <p>{{ add(5,6) }}</p> <div class="box" v-show="isShow"></div> <div class="box2" v-if="isShow"></div> <div> <button v-on:click="handlerClick">隱藏或者顯示</button> </div> <div> <!-- 首先你會發現其實指令系統的值和我們模板語法一樣,也支援各種操作,這裡我們使用了js的一個隨機數方法,並且和0.5進行比較,大於0.5的時候結果為true,那麼就會顯示'有了',如果為false就會顯示'沒了',那麼我們每次刷新頁面的時候,顯示的內容可能會發生變化,這就是v-if和v-else的用法 --> <div v-if="Math.random() > 0.5"> 有了 </div> <div v-else> 沒了 </div> </div> </div> <script src="vue.js"></script> <script> new Vue({ el:'#content', data(){ return{ msg:'<h2>超</h2>', isShow:true, } }, methods:{ add(x,y){ return x+y; }, handlerClick(){ this.isShow = !this.isShow; } } }) </script> </body> </html>

在vue2.1.0版本之後,又添加了v-else-if,v-else-if
,顧名思義,充當 v-if
的「else-if 塊」,可以連續使用:

<div v-if="type === 'A'"> A </div> <div v-else-if="type === 'B'"> B </div> <div v-else-if="type === 'C'"> C </div> <div v-else> Not A/B/C </div>

類似於 v-else
,v-else-if
也必須緊跟在帶 v-if
或者 v-else-if
的元素之後。
3.3 v-bind和v-on
直接看例子吧:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>test vue</title> <style> .box{ height: 200px; width: 200px; background-color: red; } .box2{ height: 200px; width: 200px; background-color: green; } .active{ background-color: green; height: 100px; width: 100px; } </style> </head> <body> <div id="app"> <!--1. v-bind 能夠綁定標籤所有的屬性 比如img標籤的src alt等,啊標籤的href id class title等 --> <!-- 用一個img標籤的src和alt屬性來試一下,寫法,v-bind:屬性='字元串(data裡面return裡面的屬性)' --> <!--<img v-bind:src="imgSrc" v-bind:alt="imgAlt">--> <!--2. 在來一個通過控制class屬性的值來讓標籤的css樣式動態變化 --> <!-- 注意寫法,v-bind:class='{class屬性值:Vuew對象的data裡面return中的屬性}',現在的意思是如果isActive的值為true,那麼會將active添加的前面的class屬性的值中,變成class='box active' ,如果isActive的值為false則不添加,不管是否已經寫了class屬性,寫了就是添加操作,沒寫就是創建屬性操作--> <!--<div v-bind:class="{active:isActive}"></div>--> <!--<div class="box" v-bind:class="{active:isActive}"></div>--> <!--3. 我們再來一個button標籤來點擊讓下面的div標籤動態的增減class值來變化css樣式效果,還要注意,o-on來綁定事件函數,都在Vue對象的methods屬性裡面聲明--> <!--<button v-on:click="handlerChange">點擊</button>--> <!--<div class="box" v-bind:class="{active:isActive}"></div>--> <!--4. v-bind和v-on有簡單的寫法,v-bind:(簡寫就寫一個冒號:) v-on:(簡寫就寫一個@) --> <img :src="imgSrc" :alt="imgAlt"> <!--<button @click="handlerChange">點擊</button>--> <!-- 綁定多個事件 --> <button @click="handlerChange" @mouseenter="handlerEnter" @mouseleave="handlerLeave">點擊</button> <div class="box" :class="{active:isActive}"></div> </div> <script src="vue.js"></script> <script> // 數據驅動視圖,設計模式:MVVM:Model-->View-->ViewModel //vue稱為聲明式的JavaScript框架,聲明了什麼屬性,HTML裡面就是用什麼屬性,你在頁面上一看到@就知道後面綁定了一個方法,一看到:就知道後面綁定了一個屬性,這就叫做聲明式,之前我們通過原生的js或者jQuery都是命令式的,讓它做什麼它就做什麼,了解一下就行啦 new Vue({ el:'#app', data(){ return{ //以後通過後端獲取到數據,就能夠通過更改這些數據來動態的顯示圖片等資訊了 imgSrc:'timg.jpg', //圖片路徑 // imgSrc:'timg2.jpg', imgAlt:'美女', //圖片未載入成功時的描述 isActive:true, } }, methods:{ //滑鼠點擊時的觸發效果 handlerChange(){ this.isActive=!this.isActive; }, //滑鼠進入事件的觸發效果 handlerEnter(){ this.isActive=false; }, //滑鼠離開時的觸發效果 handlerLeave(){ this.isActive=true; } } }) </script> </body> </html>

總結:
v-bind可以綁定標籤中的任意屬性
v-on可以監聽js的所有事件
MVVM框架模型圖解:(注意:以後工作的時候,前後端應該先商量好後端給什麼樣子的數據結構,前端再怎麼處理)

3.4 v-for
循環,直接看例子:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>test vue</title> </head> <body> <div id="app"> <ul v-if="data.status === 'ok'"> <!-- 遍曆數組,item表示下面列表中的每個字典 --> <!--<li v-for="item in data.users"></li> --> <!-- item和index表示下面列表中的每個字典中的鍵和值 --> <!--<li v-for="(item,index) in data.users">--> <!--<h3>{{ item.id }}--{{ item.name }}--{{ item.age }}</h3>--> <!--</li>--> <!-- v-for不僅可以遍曆數組,還可以遍歷對象,這裡大家記住v-for裡面的一個東西 :key, 就是v-bind:key,這個key是幹什麼的呢,就是為了給現在已經渲染好的li標籤做個標記,以後即便是有數據更新了,也可以在這個li標籤裡面進行數據的更新,不需要再讓Vue做重新生成li標籤的dom操作,提高頁面渲染的性能,因為我們知道頻繁的添加刪除dom操作對性能是有影響的,我現在將數據中的id綁定到這裡,如果數據裡面有id,一般都用id,如果沒有id,就綁定v-for裡面那個index(當然你看你給這個索引取的變數名是什麼,我這裡給索引取的名字是index),這裡面它用的是diff演算法,回頭再說這個演算法 --> <!-- <li v-for="(item,index) in data.users" :key="item.id" @click> 還可以綁定事件 --> <li v-for="(item,index) in data.users" :key="item.id"> <!-- v-for的優先順序最高,先把v-for遍歷完,然後給:key加數據,還有,如果沒有bind這個key,有可能你的頁面都後期用動態數據渲染的時候,會出現問題,所以以後大家記著,一定寫上v-bind:key --> <h3>{{ item.id }}--{{ item.name }}--{{ item.age }}</h3> </li> </ul> <!-- 2. 循環對象,循環對象的時候,注意寫法,值在前,key在後,如果只寫一個變數,那麼這個變數循環出來的是值 --> <!--<div v-for="value in person">--> <!--{{ value }}--> <!--</div>--> <div v-for="(value,key) in person"> {{ value }} -- {{ key }} </div> </div> <script src="vue.js"></script> <script> new Vue({ el:'#app', data(){ return{ //一般後端返回的數據都叫做data data:{ status:'ok', //返回ok表示成功 users:[ {id:1,name:'chao',age:18}, {id:2,name:'jaden',age:38}, {id:3,name:'yue',age:28}, ] }, //變臉這個對象的屬性 person:{ name:'超', } } }, methods:{ } }) </script> </body> </html>

看效果:

四 xxx
五 xxx
六 xxx
七 xxx
八 xxx
基本用法
ES6 新增了let
命令,用來聲明變數。它的用法類似於var
,但是所聲明的變數,只在let
命令所在的程式碼塊內有效。
{ let a = 10; var b = 1; } a // ReferenceError: a is not defined. b // 1
上面程式碼在程式碼塊之中,分別用let
和var
聲明了兩個變數。然後在程式碼塊之外調用這兩個變數,結果let
聲明的變數報錯,var
聲明的變數返回了正確的值。這表明,let
聲明的變數只在它所在的程式碼塊有效。
for
循環的計數器,就很合適使用let
命令。
for (let i = 0; i < 10; i++) { // ... } console.log(i); // ReferenceError: i is not defined
上面程式碼中,計數器i
只在for
循環體內有效,在循環體外引用就會報錯。
下面的程式碼如果使用var
,最後輸出的是10
。
var a = []; for (var i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 10
上面程式碼中,變數i
是var
命令聲明的,在全局範圍內都有效,所以全局只有一個變數i
。每一次循環,變數i
的值都會發生改變,而循環內被賦給數組a
的函數內部的console.log(i)
,裡面的i
指向的就是全局的i
。也就是說,所有數組a
的成員裡面的i
,指向的都是同一個i
,導致運行時輸出的是最後一輪的i
的值,也就是 10。
如果使用let
,聲明的變數僅在塊級作用域內有效,最後輸出的是 6。
var a = []; for (let i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 6
上面程式碼中,變數i
是let
聲明的,當前的i
只在本輪循環有效,所以每一次循環的i
其實都是一個新的變數,所以最後輸出的是6
。你可能會問,如果每一輪循環的變數i
都是重新聲明的,那它怎麼知道上一輪循環的值,從而計算出本輪循環的值?這是因為 JavaScript 引擎內部會記住上一輪循環的值,初始化本輪的變數i
時,就在上一輪循環的基礎上進行計算。
另外,for
循環還有一個特別之處,就是設置循環變數的那部分是一個父作用域,而循環體內部是一個單獨的子作用域。
for (let i = 0; i < 3; i++) { let i = 'abc'; console.log(i); } // abc // abc // abc
上面程式碼正確運行,輸出了 3 次abc
。這表明函數內部的變數i
與循環變數i
不在同一個作用域,有各自單獨的作用域。
不存在變數提升
var
命令會發生「變數提升」現象,即變數可以在聲明之前使用,值為undefined
。這種現象多多少少是有些奇怪的,按照一般的邏輯,變數應該在聲明語句之後才可以使用。
為了糾正這種現象,let
命令改變了語法行為,它所聲明的變數一定要在聲明後使用,否則報錯。
// var 的情況 console.log(foo); // 輸出undefined var foo = 2; // let 的情況 console.log(bar); // 報錯ReferenceError let bar = 2;
上面程式碼中,變數foo
用var
命令聲明,會發生變數提升,即腳本開始運行時,變數foo
已經存在了,但是沒有值,所以會輸出undefined
。變數bar
用let
命令聲明,不會發生變數提升。這表示在聲明它之前,變數bar
是不存在的,這時如果用到它,就會拋出一個錯誤。
暫時性死區
只要塊級作用域記憶體在let
命令,它所聲明的變數就「綁定」(binding)這個區域,不再受外部的影響。
var tmp = 123; if (true) { tmp = 'abc'; // ReferenceError let tmp; }
上面程式碼中,存在全局變數tmp
,但是塊級作用域內let
又聲明了一個局部變數tmp
,導致後者綁定這個塊級作用域,所以在let
聲明變數前,對tmp
賦值會報錯。
ES6 明確規定,如果區塊中存在let
和const
命令,這個區塊對這些命令聲明的變數,從一開始就形成了封閉作用域。凡是在聲明之前就使用這些變數,就會報錯。
總之,在程式碼塊內,使用let
命令聲明變數之前,該變數都是不可用的。這在語法上,稱為「暫時性死區」(temporal dead zone,簡稱 TDZ)。
if (true) { // TDZ開始 tmp = 'abc'; // ReferenceError console.log(tmp); // ReferenceError let tmp; // TDZ結束 console.log(tmp); // undefined tmp = 123; console.log(tmp); // 123 }
上面程式碼中,在let
命令聲明變數tmp
之前,都屬於變數tmp
的「死區」。
「暫時性死區」也意味著typeof
不再是一個百分之百安全的操作。
typeof x; // ReferenceError let x;
上面程式碼中,變數x
使用let
命令聲明,所以在聲明之前,都屬於x
的「死區」,只要用到該變數就會報錯。因此,typeof
運行時就會拋出一個ReferenceError
。
作為比較,如果一個變數根本沒有被聲明,使用typeof
反而不會報錯。
typeof undeclared_variable // "undefined"
上面程式碼中,undeclared_variable
是一個不存在的變數名,結果返回「undefined」。所以,在沒有let
之前,typeof
運算符是百分之百安全的,永遠不會報錯。現在這一點不成立了。這樣的設計是為了讓大家養成良好的編程習慣,變數一定要在聲明之後使用,否則就報錯。
有些「死區」比較隱蔽,不太容易發現。
function bar(x = y, y = 2) { return [x, y]; } bar(); // 報錯
上面程式碼中,調用bar
函數之所以報錯(某些實現可能不報錯),是因為參數x
默認值等於另一個參數y
,而此時y
還沒有聲明,屬於「死區」。如果y
的默認值是x
,就不會報錯,因為此時x
已經聲明了。
function bar(x = 2, y = x) { return [x, y]; } bar(); // [2, 2]
另外,下面的程式碼也會報錯,與var
的行為不同。
// 不報錯 var x = x; // 報錯 let x = x; // ReferenceError: x is not defined
上面程式碼報錯,也是因為暫時性死區。使用let
聲明變數時,只要變數在還沒有聲明完成前使用,就會報錯。上面這行就屬於這個情況,在變數x
的聲明語句還沒有執行完成前,就去取x
的值,導致報錯」x 未定義「。
ES6 規定暫時性死區和let
、const
語句不出現變數提升,主要是為了減少運行時錯誤,防止在變數聲明前就使用這個變數,從而導致意料之外的行為。這樣的錯誤在 ES5 是很常見的,現在有了這種規定,避免此類錯誤就很容易了。
總之,暫時性死區的本質就是,只要一進入當前作用域,所要使用的變數就已經存在了,但是不可獲取,只有等到聲明變數的那一行程式碼出現,才可以獲取和使用該變數。
不允許重複聲明
let
不允許在相同作用域內,重複聲明同一個變數。
// 報錯 function func() { let a = 10; var a = 1; } // 報錯 function func() { let a = 10; let a = 1; }
因此,不能在函數內部重新聲明參數。
function func(arg) { let arg; } func() // 報錯 function func(arg) { { let arg; } } func() // 不報錯
塊級作用域
為什麼需要塊級作用域?
ES5 只有全局作用域和函數作用域,沒有塊級作用域,這帶來很多不合理的場景。
第一種場景,內層變數可能會覆蓋外層變數。
var tmp = new Date(); function f() { console.log(tmp); if (false) { var tmp = 'hello world'; } } f(); // undefined
上面程式碼的原意是,if
程式碼塊的外部使用外層的tmp
變數,內部使用內層的tmp
變數。但是,函數f
執行後,輸出結果為undefined
,原因在於變數提升,導致內層的tmp
變數覆蓋了外層的tmp
變數。
第二種場景,用來計數的循環變數泄露為全局變數。
var s = 'hello'; for (var i = 0; i < s.length; i++) { console.log(s[i]); } console.log(i); // 5
上面程式碼中,變數i
只用來控制循環,但是循環結束後,它並沒有消失,泄露成了全局變數。
ES6 的塊級作用域
let
實際上為 JavaScript 新增了塊級作用域。
function f1() { let n = 5; if (true) { let n = 10; } console.log(n); // 5 }
上面的函數有兩個程式碼塊,都聲明了變數n
,運行後輸出 5。這表示外層程式碼塊不受內層程式碼塊的影響。如果兩次都使用var
定義變數n
,最後輸出的值才是 10。
ES6 允許塊級作用域的任意嵌套。
{{{{{let insane = 'Hello World'}}}}};
上面程式碼使用了一個五層的塊級作用域。外層作用域無法讀取內層作用域的變數。
{{{{ {let insane = 'Hello World'} console.log(insane); // 報錯 }}}};
內層作用域可以定義外層作用域的同名變數。
{{{{ let insane = 'Hello World'; {let insane = 'Hello World'} }}}};
塊級作用域的出現,實際上使得獲得廣泛應用的立即執行函數表達式(IIFE)不再必要了。
// IIFE 寫法 (function () { var tmp = ...; ... }()); // 塊級作用域寫法 { let tmp = ...; ... }
塊級作用域與函數聲明
函數能不能在塊級作用域之中聲明?這是一個相當令人混淆的問題。
ES5 規定,函數只能在頂層作用域和函數作用域之中聲明,不能在塊級作用域聲明。
// 情況一 if (true) { function f() {} } // 情況二 try { function f() {} } catch(e) { // ... }
上面兩種函數聲明,根據 ES5 的規定都是非法的。
但是,瀏覽器沒有遵守這個規定,為了兼容以前的舊程式碼,還是支援在塊級作用域之中聲明函數,因此上面兩種情況實際都能運行,不會報錯。
ES6 引入了塊級作用域,明確允許在塊級作用域之中聲明函數。ES6 規定,塊級作用域之中,函數聲明語句的行為類似於let
,在塊級作用域之外不可引用。
function f() { console.log('I am outside!'); } (function () { if (false) { // 重複聲明一次函數f function f() { console.log('I am inside!'); } } f(); }());
上面程式碼在 ES5 中運行,會得到「I am inside!」,因為在if
內聲明的函數f
會被提升到函數頭部,實際運行的程式碼如下。
// ES5 環境 function f() { console.log('I am outside!'); } (function () { function f() { console.log('I am inside!'); } if (false) { } f(); }());
ES6 就完全不一樣了,理論上會得到「I am outside!」。因為塊級作用域內聲明的函數類似於let
,對作用域之外沒有影響。但是,如果你真的在 ES6 瀏覽器中運行一下上面的程式碼,是會報錯的,這是為什麼呢?
原來,如果改變了塊級作用域內聲明的函數的處理規則,顯然會對老程式碼產生很大影響。為了減輕因此產生的不兼容問題,ES6 在附錄 B裡面規定,瀏覽器的實現可以不遵守上面的規定,有自己的行為方式。
- 允許在塊級作用域內聲明函數。
- 函數聲明類似於
var
,即會提升到全局作用域或函數作用域的頭部。 - 同時,函數聲明還會提升到所在的塊級作用域的頭部。
注意,上面三條規則只對 ES6 的瀏覽器實現有效,其他環境的實現不用遵守,還是將塊級作用域的函數聲明當作let
處理。
根據這三條規則,在瀏覽器的 ES6 環境中,塊級作用域內聲明的函數,行為類似於var
聲明的變數。
// 瀏覽器的 ES6 環境 function f() { console.log('I am outside!'); } (function () { if (false) { // 重複聲明一次函數f function f() { console.log('I am inside!'); } } f(); }()); // Uncaught TypeError: f is not a function
上面的程式碼在符合 ES6 的瀏覽器中,都會報錯,因為實際運行的是下面的程式碼。
// 瀏覽器的 ES6 環境 function f() { console.log('I am outside!'); } (function () { var f = undefined; if (false) { function f() { console.log('I am inside!'); } } f(); }()); // Uncaught TypeError: f is not a function
考慮到環境導致的行為差異太大,應該避免在塊級作用域內聲明函數。如果確實需要,也應該寫成函數表達式,而不是函數聲明語句。
// 函數聲明語句 { let a = 'secret'; function f() { return a; } } // 函數表達式 { let a = 'secret'; let f = function () { return a; }; }
另外,還有一個需要注意的地方。ES6 的塊級作用域允許聲明函數的規則,只在使用大括弧的情況下成立,如果沒有使用大括弧,就會報錯。
// 不報錯 'use strict'; if (true) { function f() {} } // 報錯 'use strict'; if (true) function f() {}
const 命令
基本用法
const
聲明一個只讀的常量。一旦聲明,常量的值就不能改變。
const PI = 3.1415; PI // 3.1415 PI = 3; // TypeError: Assignment to constant variable.
上面程式碼表明改變常量的值會報錯。
const
聲明的變數不得改變值,這意味著,const
一旦聲明變數,就必須立即初始化,不能留到以後賦值。
const foo; // SyntaxError: Missing initializer in const declaration
上面程式碼表示,對於const
來說,只聲明不賦值,就會報錯。
const
的作用域與let
命令相同:只在聲明所在的塊級作用域內有效。
if (true) { const MAX = 5; } MAX // Uncaught ReferenceError: MAX is not defined
const
命令聲明的常量也是不提升,同樣存在暫時性死區,只能在聲明的位置後面使用。
if (true) { console.log(MAX); // ReferenceError const MAX = 5; }
上面程式碼在常量MAX
聲明之前就調用,結果報錯。
const
聲明的常量,也與let
一樣不可重複聲明。
var message = "Hello!"; let age = 25; // 以下兩行都會報錯 const message = "Goodbye!"; const age = 30;
本質
const
實際上保證的,並不是變數的值不得改動,而是變數指向的那個記憶體地址所保存的數據不得改動。對於簡單類型的數據(數值、字元串、布爾值),值就保存在變數指向的那個記憶體地址,因此等同於常量。但對於複合類型的數據(主要是對象和數組),變數指向的記憶體地址,保存的只是一個指向實際數據的指針,const
只能保證這個指針是固定的(即總是指向另一個固定的地址),至於它指向的數據結構是不是可變的,就完全不能控制了。因此,將一個對象聲明為常量必須非常小心。
const foo = {}; // 為 foo 添加一個屬性,可以成功 foo.prop = 123; foo.prop // 123 // 將 foo 指向另一個對象,就會報錯 foo = {}; // TypeError: "foo" is read-only
上面程式碼中,常量foo
儲存的是一個地址,這個地址指向一個對象。不可變的只是這個地址,即不能把foo
指向另一個地址,但對象本身是可變的,所以依然可以為其添加新屬性。
下面是另一個例子。
const a = []; a.push('Hello'); // 可執行 a.length = 0; // 可執行 a = ['Dave']; // 報錯
上面程式碼中,常量a
是一個數組,這個數組本身是可寫的,但是如果將另一個數組賦值給a
,就會報錯。
如果真的想將對象凍結,應該使用Object.freeze
方法。
const foo = Object.freeze({}); // 常規模式時,下面一行不起作用; // 嚴格模式時,該行會報錯 foo.prop = 123;
上面程式碼中,常量foo
指向一個凍結的對象,所以添加新屬性不起作用,嚴格模式時還會報錯。
除了將對象本身凍結,對象的屬性也應該凍結。下面是一個將對象徹底凍結的函數。
var constantize = (obj) => { Object.freeze(obj); Object.keys(obj).forEach( (key, i) => { if ( typeof obj[key] === 'object' ) { constantize( obj[key] ); } }); };