本節目錄
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很多公司也在用,是谷歌開發的,老項目一般是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框架+mysql\redis等)
畫一個django和vue的對比圖吧
由於后面學習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>
1.下載安裝
我們使用vue就要把人家下載下來安裝一下,就像你使用django框架一樣,需要下載安裝django才能使用,這個vue框架小而精,但是功能很強大,之前我們的js或者jQuery操作基本都可以通過vue來完成,我們下面是按照vue2.x學的,如果vue更新為3.0了,那么大家記得去學學里面的新語法。
方式1:
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/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對象自帶的屬性(el\data\methods等,會在屬性名前面加一個$符號,以便和我們自定義的屬性(msg\isShow\add等等)區分) 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對象自帶的屬性(el\data\methods等,會在屬性名前面加一個$符號,以便和我們自定義的屬性(msg\isShow\add等等)區分) 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>
看效果:
基本用法
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] );
}
});
};