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很多公司也在用,是谷歌开发的,老项目一般是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 edis等)
画一个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; 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; 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; 200px; background-color: red; } .box2{ height: 200px; 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; 200px; background-color: red; } .box2{ height: 200px; 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; 200px; background-color: red; } .box2{ height: 200px; 200px; background-color: green; } .active{ background-color: green; height: 100px; 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] );
}
});
};