@hpp157
2016-08-31T15:13:13.000000Z
字数 11755
阅读 3232
React
从1995年发布以来,javascript经历了很大变化。起初,它只是用来为网页添加简单的互动元素的,后来随着DHTML和AJAX兴起迎来来爆发式的增长,现在随着nodejs的出现,它已经成为了可以开发全栈应用的一门编程语言。负责带领javascript发展的委员会叫做ECMA,欧洲计算机制造商协会。
js语言的变化是靠社区驱动的,最初的建议都是js社区成员写成的。任何人都可以向ECMA提交建议,ECMA委员会的职责就是管理和对建议进行排序,决定采用什么建议等。建议由明确定义的阶段来划分,stage0代表最新的提议,stage4代表完成的提议。
最近完成的版本规范是2015年6月,有许多叫法,ECMAScript6,ES2015,Harmony或者ESNext,每隔一年周期发布一个规范,2016年,发布规范变化比较小,但是仍会包含一些有用的功能。我们会在本书中使用许多这些新的功能。
许多新特性已经被最新的浏览器支持,我们也会告诉你怎样将最新的js代码转换成符合ES5标准的代码,让它可以在今天运行于大多数的浏览器中。
这一章,我们会向你展示所有的最新代码,如果你还没有学习js的新变化,现在就是学习它的最好时机。如果你已经适应了ES6的语法特性,你可以跳到下一章。
ES6引入了常量,许多浏览器也已经支持这一特性。常量就是不会被改变的变量。它和其它语言中的常量概念没有什么不同。
除了常量,其它的都是变量,变量可以被重写覆盖。
var pizza = truepizza = falseconsole.log(pizza) //fasle
我们不能重置常量的value,如果你尝试这么做了,控制台会抛出一个错误。
const pizza = truepizza = falseconsole.log(pizza) //uncaught TypeError:assignment to constant variable
javascript现在有了词法变量作用域(lexical variable scope),在javascript中我们用花括号'{}'编写代码块,函数中,花括号封闭了变量作用域。另一方面,想下if/else语句,如果你学过其它语言的话,你会发现if/else代码块和块级作用域很相似。事实并非如此。
如果一个变量在if/esle代码块中创建,那么这个变量并不在这个代码块范围内。
var topic = 'javascript'if(topic){var topic = 'react'console.log('block',topic) //block react}console.log('global',topic) //global react
这个if/else 语句中的变量topic,重置了全局变量topic的值
使用关键字let的话,可以引用任何一个代码块中的变量,使用let可以保护全局变量
var topic = 'javascript'if(topic){let topic = 'react'console.log('block',topic) //block react}console.log('global',topic) //global react
代码块外部的全局变量topic没有被重置
另一个花括号封闭不了变量作用域的地方是在for循环中
var div ,container = document.getElementById('container')for (var i=0;i<5;i++){div =document.createElement('div')div.onclick = function(){alert('this is box #' + i)}container.appendChild(div)}
在这个for循环中,我们创建了5个div显示在一个container中,每个div在点击的时候出发一个alert函数,显示它是第几个div。for循环中,声明的i,循环创建了叫一个做i的全局变量,然后迭代5次,当你点击任何一个div的时候,alert都会说i等于5,因为目前的全局变量是5
在for循环中给计数器i用let来声明后,点击第一个div,alert是0,第二个alert就显示2,正常了。
模板字符串给我们提供了一种字符串链接的方法,它允许我们在字符串插入一个变量。
传统的字符串链接用的是+符号,或者在字符串和变量之间使用一个逗号链接。
console.log(lastName + ',' + firstName + ' ' +middleName)
使用模板字符串的话,我们可以创建一个字符串并且用${variable}的方式插入一个变量。
console.log('${lastNanme},${firstName} ${middleName}')
模板字符串中可以直接使用空格,这使得我们在制作一个邮件模板,代码举例,或者任何包含空格的地方都非常容易。
hello ${firstName},Thanks for ordering ${qty} tickets to ${event}.Order Details${firstName} ${middleName} ${lastName}${qty} * $${price} = ${qty *price} to ${event}You can pick your tickets up at will call 30 minutes beforethe show.Thanks,${ticketAgent}
以前,在我们js代码中使用html字符串并不容易,因为我们需要保证让它们在同一行上。
现在空格被js解释器识别为文本,你插入格式化的html之后也很容易让人理解。
document.body.innerHTML='<section><header><h1>The Html Blog</h1></header><article><h2>${article.title}</h2>${article.body}</article><footer><p>copyright ${new Date().getYear()} | The HTML5 Blog</p></footer></section>'
在C++和Python语言中,函数参数是可以声明默认值的,ES6的规范中也增加了默认参数,因此,如果函数的参数没有提供值的话,就会使用默认的值。
function logActivity(name="Shane McConkey",activity="skiing") {console.log('${name} loves ${activity}')}
如果调用时不给这个函数提供参数,它就会使用默认值,默认值可以是任何的类型,不仅仅是字符串
var defaultPerson={name:{first:'Shane',last:'McConkey'}favActivity:'skiing'}function logActivity(p=defaultPerson) {console.log('${p.name.first} loves ${p.favActivity}')}
箭头函数是ES6中非常有用的一个特性,它可以让你声明一个函数,不用写functon关键字。也不用写return关键字。
传统的函数
var lordify = function(){return '${firstName} of Canterbury'}console.log(lordify('dale')) //dale of Canterburyconsole.log(lordify('Daryld')) //Daryld of Canterbury
使用箭头函数可以极大的简化语法:
var lordify = firstname => '${firstname} of Canterbury '
我们现在用箭头函数,把整个函数声明成了一行的代码,function这个关键字被移除了,我们也把return去掉来,因为箭头函数以经指向了返回值。另一个好处是,如果我们的函数只有一个参数,我们可以去掉参数周围的括号
//oldvar lordify = function(firstName, land){return '${firstName} of ${land}'}//newvar lordify = (firstName,land) => '${firstName} of ${land}'
只有一个返回语句的函数可以写成一行,超过一行的需要{}包裹。
var_lordify = (firstName,land) => {if (!firstName){throw new Error('a firstName is required')}if (!land){throw new Error('a land is required')}return '${firstName} of ${land}'}
这些函数并不封闭this,比如,下面例子中'this'指的的别的东西,而不是tahoe这个对象
var tahoe ={resorts:["kirkwood","squaw","alpine","Heavenly","Northstar"]print:function(delay=1000){setTimeout(function(){console.log(this.resorts.json(","))},delay)}}tahoe.print() //kirkwood,squaw,alpine,Heavenly,Northstar
这段代码可以正常运行,我们也用逗号把结果连起来并进行来排序。一定要小心,即使你一直把作用域的事情记在脑子里。箭头函数并不封闭this
var tahoe ={resorts:["kirkwood","squaw","alpine","Heavenly","Northstar"]print:(delay=1000) => {setTimeout(() => {console.log(this.resorts.json(","))},delay)}}tahoe.print() //Cannot read property resorts of undefined
出错了,把print函数链接到一个箭头函数意味着this,指的是window。
我们可以用下面代码验证下:
var tahoe ={resorts:["kirkwood","squaw","alpine","Heavenly","Northstar"]print:(delay=1000) => {setTimeout(() => {console.log(this === window)},delay)}}tahoe.print() //true
它显示为true,为了修改它,我们可以只使用一个常规函数
var tahoe ={resorts:["kirkwood","squaw","alpine","Heavenly","Northstar"]print:function(delay=1000){setTimeout(() => {console.log(this === window)},delay)}}tahoe.print() //false
尽管术语“traspiling"从上世纪就已经出现了,但是似乎对于它到底是什么意思以及”transpiling"和“compiling"有什么区别仍然存在很多疑惑。
首先,transpiling是一种特殊的compiling。这很有助于让我们了解到我们是在谈论差不多类似的东西,实际上它就是一种很特别的编译。但区别于通常意义的编译我们该如何定义它呢?
Compiling这个术语通常是将一种语言编写的源代码转换为另一种。
Transpiling是一个特定的术语,用于将一种语言编写的源代码转换为另一种具有相同抽象层次的语言。
因此(简单来说)当你编译C#时,编译器将函数体转换为中间语言(IL)。这不能称为transpiling,因为这两种语言的抽象层次完全不同。
当你编译TypeScript时,编译器将它转换为JavaScript。二者的抽象层次相同,所以你可以称之为transpiling。
编译器和transpiler都可以在处理过程中优化代码。
其他一些常见的可以称为transpiling的组合包括C++到C,CoffeeScript到JavaScript,Dart到JavaScript以及PHP到C++。
Babel
大部分的浏览器不支持ES6,要保证你的ES6代码可以工作的唯一方法就是在浏览器运行代码前把它转成ES5代码,这个过程叫做编译(transpiling),Babel是最著名的编译工具之一。
过去,要想使用最新的代码的唯一方法是就是等待,等一月,一年,一直到浏览器支持它们。现在编译让你立即运行最新特性的js代码成为可能。transpiling不是compiling,它没有把代码转换成二进制格式。
这儿有一段ES6代码,默认参数,箭头函数这些新特性都有,
const add =(x=5, y=10) => console.log(x+y);
转换以后是这样的
‘use strict'var add = function add(){var x = arguments.length <=0 || arguments[0] === undefined ?5:arguments[0]var y = arguments.length <=1 || arguments[1] === undefined ?10:arguments[1]return console.log(x+y);
转换器添加了一个use strict的声明,变量x默认使用参数数组。这(Babel)是一个你应该熟悉的工具。这个结果是大多数浏览器可以运行的。
你可以通过行内Babel转换器直接把js代码转换到浏览器中。只需要导入browser.js文件,所有用 type="text/babel"的类型都会被转换,Babel6是目前的最新版本.
<script src="//cdn.bootcss.com/babel-core/6.1.17/browser.js"></script>
这种方法实质上是在浏览器在运行时进行转换工作,这对于应用来说并不是一个好的注意,因为它会降低应用的反应速度。第5章中,我们会继续介绍在产品中怎么解决这个问题。
你可能这样想过,太棒了,当大多数浏览器支持ES6的时候,我们就不需要用Babel了,可是,迟早我们要用ES7的新特性来工作,还是会需要转换,未来的很长时间都要用到它。
ES6提供了让变量作用域保持在对象和数组这些数据集内部的新方法,新特性包括解构(destructuring),增强的对象字面量(object literal enhancement) 和扩展操作符(spread operator)。
解构赋值,允许你截出对象或数组的一部分来使用,思考这个sandwich对象,它有4个key,但是我们只需要bread和meat两个key的值。
var sandwich ={bread:"dutch crunch",meat:"tuna",cheese:"swiss",tippings:["lettuce","tomato","mustard"]}var {bread,meat} = sandwichconsole.log(bread,meat) //dutch crunch tuna
这段代码从对象中取出bread和meat,并且为它们创建本地变量,当然,bread和meat的变量可以被改变。
var {bread,meat} = sandwichbread = "garlic"meat ="tukey"console.log(bread) //garlicconsole.log(meat) //turkeyconsole.log(sandwich.bread, sandwich.meat) //dutch crunch tuna
我们也可以解构传入的函数参数(从传入的函数参数中截取出仅自己需要的值),思考下这个函数,它会记录一个人的名字作为主语。
//oldvar lordify = regularPerson =>{console.log('${regularPerson.firstname} of Canterbury')}var regularPerson ={firstname:"Bill",lastName:"wilson"}lordify(regularPerson) //Bill of Canterbury
相比使用点语法来获得值的这种方法外,我们解构函数的参数:
var lordify = regularPerson =>{console.log('${firstname} of Canterbury')}
通过结构firstName,我们声明只使用firstName变量,更多关于声明式的编程会在下一章介绍。
还可以从数组中解构,想象一下,我们要把数组的第一个值赋给一个变量名,
var [firstResort] = ["kirkwood","squaw","alpine"]console.log(firstResort) //kirwood
你也可以用逗号略去不需要的值,当开始匹配时,逗号代替的元素会被忽略。
上一个数组,我们可以通过用逗号代替前两个值的方法来访问第三个值
var [,,thirdtResort] = ["kirkwood","squaw","alpine"]console.log(thirdResort) //alpine
在稍后的章节中,我们会通过把数组解构和扩展操作符结合在一起来更深入的讨论这个例子。
增强的对象字面量是解构的相反面,它是重组和放置回一块儿的过程。通过增强的对象字面量,我们可以从一个作用域(scope)中抓取变量然后把它们放进一个对象。
var name = "Tallac"var elevation = 9738var funHike ={name,elevation}console.log(funHike) //{name:"Tallac",elevation:"99738"}
name和elevation现在是funHike对象的key了。
我们也可以用增强的对象字面量或者重组来创建对象的方法。
var name = "Tallac"var elevation = 9738var print = function(){console.log('Mt.${this.name} is ${this.elevation} feet tall')}var funHike =(name,elevation,print)funHike.print() //Mt.Tallac is 9738 feet tall
注意,我们用this获取对象的key
当定义对象的方法时,function关键字不再是必须的了。
下面是新旧方法对比
var skier {name:name,sound:sound,powerYell:function(){var yell = this.sound.toUpperCase()console.log('${yell} ${yell} ${yell} !!!')},speed:function(mph){this.speed =mphconsole.log('speed:',mph)}}//下面是新的方法var skier {name:name,sound:sound,powerYell(){let yell = this.sound.toUpperCase()console.log('${yell} ${yell} ${yell} !!!')},speed(mph){this.speed =mphconsole.log('speed:',mph)}}
增强的对象字面量可以把全局变量拉取到对象中,以及可以省去关键字function少写点字
扩展操作符是三个点号...执行一些不同的工作,首先,扩展操作符允许我们合并数组的内容。比如,我们有两个数组,我们可以合并它们两个为一个来创造第三个数组。
var peaks =["Tallac","Ralston","Rose"]var canyons =["ward","Blackwood"]var tahoe =[...peaks,...canyons]//Tallac Ralston Rose Ward Blackwood
peaks和canyons中的元素都被放进了第三个数组中
让我们看下扩展操作符是如何来解决问题的。还是用peaks这个数组作为例子,想象一下,我们只想抓取最后一项而不是第一项,我们可以使用.reverse()来反转函数,结合解构数组就可以解决这个问题
var peaks =["Tallac","Ralston","Rose"]var [last] =peaks.reverse()console.log(last) //Roseconsole.log(peaks.join(','))//Rose,Ralston,Tallac
来看下发生了什么,reverse函数改变了数组,通过使用扩展操作符,我们可以不必改变原始数组,我们可以创建一份拷贝,然后反转它。
var peaks =["Tallac","Ralston","Rose"]var [last] =[...peaks].reverse()console.log(last) //Roseconsole.log(peaks.join(','))//Tallac Ralston Rose
由于我们使用了扩展操作符复制了一个数组,所以原来的数组没有改变。
扩展操作符也可以用来获取数组中一部分,或者剩下的项目
var lakes = ["donner","marlette","fallen leaf","cascade"]var [first,...rest] =lakesconsole.log(rest.join(','))//"matlette,fallen leaf,cascade"console.log(first) //donner
我们还可以用扩展操作符把函数参数收集起来作为一个数组:
function directions(...args){var [start,...remaining] = argsvar [finish,...stops] = remaining.reverse()console.log('drive through ${args.length} towns' )console.log('start in ${start}')console.log('the destination is ${finish}')console.log('stoping ${stoping.length) times in between')}directions('Truckee','Tahoe City','Sunnyside','Homewood','Tahoma')
directions函数使用扩展操作符来收集参数,第一个参数赋值给了start变量,最后一个参数使用reverse方法赋值给finish变量,然后使用参数数组的length方法显示穿过了几个城市,stop的次数是参数数组的长度减去finish stop 。这种提供参数的方法难以想象的灵活,我们可以通过使用directions函数来处理任意数字的stops
扩展操作符也可以用于对象,这是ES7的stage2阶段要实现的目标。使用方法和数组扩展操作符相似,举例,用一个对象,一个字符串,合并成一个对象。
var morning={breakfast:"oatmeal",lunch:"peanut butter and jelly"}var dinner = "mac and cheese"var backpackingMeals ={..morning,dinner}console.log(backpackingMeals)/* {breakfast:"oatmeal",lunch:"peanut butter and jelly","mac and cheese"}*/
在早期的js中,我们写一个脚本需要许多不同的函数
Promises给我们提供了一种了解异步行为的方法。当我们发送一个异步请求的时候,一种或两种事情会发生:要么一切正常,或者失败了。这可以在许多不同的方面发生。比如,我们可能有很多种获取数据的成功方法,也可能收到各种各样的错误。Promises给我们一种简化方法,简单的返回pass或者fail(通过或者失败)。
我们创建一个promises来处理你可能遇到的各种各样的赢或输骰子的情况。
如果你从来没有玩过骰子的游戏,也没关系,我们会用代码写个例子教你怎么做。
一起,js中是没有类的概念的。类型是通过函数来定义的。我们必须创建一个函数,然后使用prototype在函数对象中定义方法。
function Vacation(destination,length){this.length = lengththis.destination = destination}Vacation.prototype.print = function(){console.log(this.destination + "|" + this.length + "days")}var maui = new Vacation("Maui",7);maui.print(); //Maui | 7
如果你熟悉经典的面向对象的话,这可能把你弄疯。
现在ES6引入了类的声明 ,但是js还是用原来的方法工作。函数即对象,通过原型来继承。但是它的语法可能更符合经典的面向对象语法(classical object orientation)。
首字母大写约定:js中的类首字母应该大写。
var maui = new Vacation("Maui",7);maui.print(); //Maui | 7class Vacation{this.destination =destinationthis.length = length}print(){console.log('${this.destination} will take $(this.length) days.')}
一旦我们创建了类,我们将使用new关键字创建一个类的实例,然后你可以在类上面定制你的方法。
const trip = new Vacation("Santiago,Chile",7)console.log(trip.printDetails())
现在一个类对象(class object)已经被创建了,只要你创建一个新的实例,你就可以它很多次。类也可以被继承,当一个类被继承的时候,子类继承超类(supper class)的属性和方法。这些属性和方法可以被在子类中修改。但是默认的,超类的属性和方法,一般会被继承。
你可以把Vacation当作抽象类,来创建Vacation的不同类型。比如,
class Expedition extends Vacation{constructor(destination,length,gear){super(destination,length)this.gear = gear}print(){super.print()console.log('bring your ${this.gear.join("and your")} ')}}
这是一个简单的继承:子类继承了超类的属性,通过调用Vacation的printDetail方法,我们可以添加一些自己希望打印出的内容,创建一个新的实例通常是这样的,创建一个变量,并且使用关键字new
const trip = new Expedition("Mt,Whitney",3,[“sunglasses","prayer flags","camera"])trip.print() //bring your sunglasses and your prayer flags and your camera
类和原型继承
使用类的本质还是在使用的js的原型继承,打印Vacation.prototype,你会注意到在原型上的consructor和printDetails方法
本书中 ,我们会使用一点类,但是我们的重点还是在函数式编程。类还有其他的属性,比如getters,setters,和静态方法。但是本书更偏爱于函数式编程而不是面向对象。我们介绍类的原因是因为会在稍后创建React时使用到它。
js实际上正在迅速的向前迭代,期间也采用工程师们的建议。浏览器也在快速的实现ES6的特性,所以毫不犹疑的使用ES6吧。