JavaScript
参考:
- wiki: https://zh.wikipedia.org/zh-cn/JavaScript
- W3school: https://www.w3school.com.cn/js/index.asp
- 廖雪峰:https://www.liaoxuefeng.com/wiki/1022910821149312
环境:
- ELRH7x86_64
简介
Introduction
JavaScript(JS)是一种解释型的高级编程语言。JavaScript是一门基于原型、函数先行的语言,是一门多范式的语言,它支持面向对象编程,命令式编程,以及函数式编程。它提供语法来操控文本、数组、日期以及正则表达式等,不支持I/O,比如网络、存储和图形等,但这些都可以由它的宿主环境提供支持。它已经由ECMA(欧洲电脑制造商协会)通过ECMAScript实现语言的标准化。它被世界上的绝大多数网站所使用,也被世界主流浏览器(Chrome、IE、Firefox、Safari、Opera)支持。
虽然JavaScript与Java这门语言不管是在名字上,或是在语法上都有很多相似性,但这两门编程语言从设计之初就有很大的不同。为什么起名叫JavaScript?原因是当时Java语言非常红火,所以网景公司希望借Java的名气来推广,但事实上JavaScript除了语法上有点像Java,其他部分基本上没啥关系。
JavaScript是世界上最流行的脚本语言,因为你在电脑、手机、平板上浏览的所有的网页,以及无数基于HTML5的手机App,交互逻辑都是由JavaScript驱动的。随着HTML5在PC和移动端越来越流行,JavaScript变得更加重要了。并且,新兴的Node.js把JavaScript引入到了服务器端,JavaScript已经变成了全能型选手。
ECMAScript
为了让JavaScript成为全球标准,几个公司联合ECMA(European Computer Manufacturers Association)组织定制了JavaScript语言的标准,被称为ECMAScript标准。
所以简单说来就是,ECMAScript是一种语言标准,而JavaScript是网景公司对ECMAScript标准的一种实现。 JavaScript的标准是ECMAScript 。ECMAScript第一版标准发布于1997年。
那为什么不直接把JavaScript定为标准呢?因为JavaScript是网景的注册商标。
快速入门
JavaScript代码可以直接嵌在网页的任何地方,不过通常我们都把JS代码放到<head>
中。由<script>...</script>
包含的代码就是JS代码,它将直接被浏览器执行。
|
|
第二种方法是把JavaScript放到单独的.js
文件,然后在HTML中通过<script src="..."></script>
来引入。
|
|
将JS代码放入单独的文件中更有利于维护代码,并且多个页面可以复用。
在同一个页面中引入多个JS文件(或编写多个JS代码),浏览器将按照顺序依次执行。
有时会看到<script>
有一个type
属性。但其实这是没有必要的,以你为默认的type
就是javascript
,所以不必显式指定。
|
|
console.log()
代替alert()
的好处是可以避免弹出烦人的对话框。
|
|
如何运行JS
要让浏览器运行JavaScript,必须先有一个HTML页面,在HTML页面中引入JavaScript。然后,然浏览器加载该HTML页面,就可以执行JavaScript代码。
基本语法
每个语句以分号;
结束,语句块使用花括号{}
。
|
|
数据类型
- 数字(Number)
- 字符串(String)
- 布尔(Bool)
- 空(Null)
- 未定义(Undefined)
- Symbol: 独一无二的值
- 数组(Array)
- 对象(Object)
- 函数(Function)
JS不区分整数和浮点数,统一用Number表示。
NaN
这个特殊的数字与所有其它值都不相等,包括它自己。
字符串以单引号或双引号括起来的任意文本。可通过转义字符()进行转义。
布尔值只有true
和false
两种值。
null
表示一个空值,如Python的None
。undefined
表示未定义。大多数情况下,我们都应该使用null
,undefined
仅仅在判断函数参数是否传递的情况下有用。
''
表示长度为0的字符串。
数组使用[]
表示,元素间用逗号,
分隔。类似于Python的List,包括所索引、切片等操作。
对象是一组由键值组成的无序集合。类似于Python的Dictionary。
|
|
Map和Set
JavaScript的默认对象表示方式{}
可以视为其他语言中的Map或Dictionary的数据结构,即一组键值对。但是JavaScript的对象有个小问题,就是键必须是字符串。但实际上Number或者其他数据类型作为键也是非常合理的。
为了解决这个问题,最新的ES6规范引入了新的数据类型Map
。
|
|
Set和Map类似,也是一组Key的集合,但不存储Value。没有重复的键。
|
|
动态类型
JavaScript拥有动态类型,这意味着相同的变量可哦你工作不同的类型。
|
|
运算符
&&
||
!
>
,<
>=
,<=
==
: 会自动转换数据类型再比较===
: 不会自动转换数据类型,如果数据类型不一致,返回false;如果一致,再比较
由于JS这个设计缺陷,不要使用==
,始终坚持使用===
。
变量
变量在JavaScript中就是用一个变量名表示,变量名是大小写英文、数字、$
和_
的组合,且不能用数字开头。变量名也不能是JavaScript的关键字。变量名也可以用中文。
|
|
可使用关键字new
来声明变量类型:
|
|
严格模式
JavaScript在设计之初,为了便于学习,并不强制要求使用var
声明变量。这个设计错误带来了严重的后果:如果一个变量没有通过var
申明就被使用,那么该变量就自动被申明为全局变量。
|
|
在同一个页面的不同的JavaScript文件中,如果都不用var
申明,恰好都使用了变量i,将造成变量i互相影响,产生难以调试的错误结果。
使用var
申明的变量则不是全局变量,它的范围被限制在该变量被申明的函数体内,同名变量在不同的函数体内互不冲突。
为了修补JavaScript这一严重设计缺陷,ECMA在后续规范中推出了严格(strict)模式,在strict模式下运行的JavaScript代码,强制通过var
申明变量,未使用var
申明变量就使用的,将导致运行错误。
不用var
申明的变量会被视为全局变量,为了避免这一缺陷,所有的JavaScript代码都应该使用strict模式。我们在后面编写的JavaScript代码将全部采用strict模式。
启用strict模式的方法是在JavaScript代码的第一行写上:'use strict';
条件判断
|
|
栗子:
|
|
JavaScript把null
, undefined
, 0
, NaN
, ''
视为fasle
,其它一概视为true
。
栗子:
|
|
循环
for
while
do...while
|
|
|
|
|
|
iterable
遍历Array
可以采用下标循环,遍历Map
和Set
就无法使用下标。为了统一集合类型,ES6标志引入了新的iterable
类型,Array, Map, Set都属于iterable
类型。
具有iterable
类型的集合可以通过新的for...of
循环来遍历。
|
|
函数
Function
借助抽象,我们才能不关心底层的具体计算过程,而直接在更高的层次上思考问题。写计算机程序也是一样,函数就是最基本的一种代码抽象的方式。
函数定义
JavaScript中,定义函数的方式如下:
|
|
JS还有一个免费赠送的关键字arguments
,它只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数。
实际上arguments
最常用于判断传入参数的个数。
|
|
ES6标准引入了rest
参数。
|
|
变量作用域
在JavaScript中,用var
申明的变量实际上是有作用域的。
如果一个变量在函数体内部申明,则该变量的作用域为整个函数体,在函数体外不可引用该变量。
变量提升
虽然JavaScript的函数有一个变量提升的特点,它会先扫描整个函数体的语句,把所有申明的变量提升到函数顶部。但我们在函数内部定义变量时,请在函数内部首先申明所有变量。
全局作用域
不在任何函数内定义的变量就具有全局作用域。实际上,JavaScript默认有一个全局对象window
,全局作用域实际上被绑定到window
的一个属性。
|
|
这说明JavaScript实际上只有一个全局作用域。任何变量(函数也视为变量),如果没有在当前函数作用域中找到,就会继续往上查找,最后如果在全局作用域中也没有找到,则报ReferenceError
错误。
命名空间
全局变量会绑定到window
上,不同的JavaScript文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,都会造成命名冲突,并且很难被发现。
减少冲突的一个方法是把自己的所有变量和函数全部绑定到一个全局变量中。例如:
|
|
把自己的代码全部放入唯一的命名空间MYAPP
中,会大大减少全局变量冲突的可能。
许多著名的JS库都是这么做的:jQuery, YUI等等。
局部作用域
由于JavaScript的变量作用域实际上是函数内部,我们在for
循环等语句块中是无法定义具有局部作用域的变量的。
为了解决块级作用域,ES6引入了新的关键字let
,用let
替代var
可以申明一个块级作用域的变量。
常量
由于var
和let
申明的是变量,如果要申明一个常量,在ES6之前是不行的,我们通常用全部大写的变量来表示这是一个常量,不要修改它的值。
|
|
ES6标准引入了新的关键字const
来定义常量,const
和let
都具有块级作用域。
|
|
解构赋值
从ES6开始,JavaScript引入了解构赋值,可以同时对一组变量进行赋值。
使用解构赋值可以减少代码量,但是,需要在支持ES6解构赋值特性的现代浏览器中才能正常运行。
方法
在一个对象中绑定函数,称为这个对象的方法。
|
|
注意,有一个this
关键字。在一个方法内部,this
是一个特殊变量,它始终指向当前对象,也就是xiaoming
这个变量。
我们可以控制this
的指向。要确定函数的this
指向哪个对象,可以用函数本身的apply
方法,它接收两个参数,第一个参数就是需要绑定的this
变量,第二个参数是Array
,表示函数本身的参数。
另一个与apply()
类似的方法是call()
,唯一区别是:
apply()
把参数打包成Array
再传入;call()
把参数按顺序传入。
栗子:
|
|
对普通函数调用,我们通常把this
绑定为null
。
装饰器
利用apply()
,我们还可以动态改变函数的行为。
JavaScript的所有对象都是动态的,即使内置的函数,我们也可以重新指向新的函数。
高阶函数
一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数(Higher order function)。
|
|
map/reduce
map()
方法定义在JS的Array
中。
|
|
reduce()
把结果继续和序列的下一个元素做累积计算。
|
|
filter
filter()
它用于把Array
的某些元素过滤掉,然后返回剩下的元素。
|
|
sort
JavaScript的Array
的sort()
方法就是用于排序的,但它默认把所有元素先转换为String再排序,如果不知道这个,那么用它直接对数字排序会栽进坑里。
其它高阶函数
Array
对象还提供了许多非常实用的高阶函数:
every()
: 判断数组的所有元素是否满足测试条件find()
: 查找符合条件的第一个元素,如果找到了,返回这个元素;否则,返回undefined
findIndex()
: 它返回查找元素的索引forEach
: 把每个元素依次作用于传入的函数,但不会返回新的数组。常用于遍历数组
闭包
闭包是一种保护私有变量的机制,在函数执行时形成私有的作用域,保护里面的私有变量不受外界干扰。直观的说就是形成一个不销毁的栈环境。
箭头函数
ES6新增了一种新的函数:箭头函数(Arrow Function)。 感觉有点类似于Python的lambda。
|
|
其它用法:
|
|
生成器
**生成器(generator)**是ES6标准引入的新的数据类型。一个生成器看上去像一个函数,但可以返回多次。
同样类似于Python的生成器,还记得next
和yield
吗?哈哈。
|
|
直接调用生成器和调用函数不一样,仅仅是创建了一个生成器对象,还没有去执行它。
|
|
标准对象
在JavaScript的世界里,一切都是对象。但某些对象还是和其它对象不一样。
|
|
JS还提供了包装对象,熟悉Java的小伙伴肯定很清楚int
和Integer
这种暧昧关系。
虽然包装对象看上去和原来的值一摸一样,但是它们的类型已经变为object
了。所以,闲的蛋疼也不要使用包装对象。
|
|
Date
在JavaScript中,Date
对象用来表示日期和时间。
注意,当前时间是浏览器从本机操作系统获取的时间,所以不一定准确,因为用户可以把当前时间设定为任何值。
JavaScript的Date对象月份值从0开始,牢记0=1月,1=2月,2=3月,……,11=12月。
|
|
时区
Date
对象表示的时间总是按浏览器所在的时区显示,不过我们既可以显示本地时间,也可以显示调整后的UTC时间。
|
|
RegExp
强大的正则表达式!
了解了基本的RE知识,我们就可以在JavaScript中使用正则表达式了。JS有两种方式创建一个正则表达式:
/正则表达式/
new RegExp('正则表达式')
,创建一个RegExp对象
|
|
切分字符串
用正则表达式切分字符串比用固定的字符更灵活。
|
|
分组
除了简单地判断是否匹配之外,正则表达式还有提取子串的强大功能。用()
表示的就是要提取的分组(Group)。
如果正则表达式中定义了组,就可以在RegExp对象上用exec()
方法提取出子串来。
|
|
贪婪匹配
需要特别指出的是,正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符。
|
|
全局搜索
JavaScript的正则表达式还有几个特殊的标志,最常用的是g
,表示全局匹配。
|
|
JSON
JSON是JavaScript Object Notation的缩写,它是一种轻量级的数据交换格式。
在JSON出现之前,大家一直用XML来传递数据。因为XML是一种纯文本格式,所以它适合在网络上交换数据。XML本身不算复杂,但是,加上DTD、XSD、XPath、XSLT等一大堆复杂的规范以后,任何正常的软件开发人员碰到XML都会感觉头大了,最后大家发现,即使你努力钻研几个月,也未必搞得清楚XML的规范。
JSON实际上是JavaScript的一个子集。在JSON中,一共就这么几种数据类型:
- number
- boolean
- string
- null
- array
- object
JSON还定死了字符集必须是UTF-8,表示多语言就没有问题了。为了统一解析,JSON的字符串规定必须用双引号""
,Object的键也必须用双引号""
。
由于JSON非常简单,很快就风靡Web世界,并且成为ECMA标准。几乎所有编程语言都有解析JSON的库,而在JavaScript中,我们可以直接使用JSON,因为JavaScript内置了JSON的解析。
把任何JavaScript对象变成JSON,就是把这个对象序列化成一个JSON格式的字符串,这样才能够通过网络传递给其他计算机。 如果我们收到一个JSON格式的字符串,只需要把它反序列化成一个JavaScript对象,就可以在JavaScript中直接使用这个对象了。
序列化
|
|
反序列化
|
|
面向对象编程
JavaScript的面向对象编程和大多数其他语言如Java、C#的面向对象编程都不太一样。面向对象的两个基本概念:
- 类:类是对象的模板
- 实例:根据类创建的对象
所以,类和实例是大多数面向对象编程语言的基本概念。
不过,在JavaScript中,这个概念需要改一改。JavaScript不区分类和实例的概念,而是通过原型(prototype)来实现面向对象编程。 JavaScript没有类的概念,所有对象都是实例。所谓继承关系不过是把一个对象的原型指向另一个对象而已。
创建对象
JavaScript对每个创建的对象都会设置一个原型,指向它的原型对象。
当我们用obj.xxx
访问一个对象的属性时,JavaScript引擎先在当前对象上查找该属性,如果没有找到,就到其原型对象上找,如果还没有找到,就一直上溯到Object.prototype
对象,最后,如果还没有找到,就只能返回undefined
。
|
|
构造函数
JavaScript还可以用一种构造函数的方法来创建对象。
|
|
要让创建的对象共享一个函数,根据对象的属性查找规则,我们只需要把此函数移动到这些对象的原型上就可以了,也就是xxx.prototype
:
|
|
为了区分普通函数和构造函数,按照约定,构造函数首字母应当大写,而普通函数首字母应当小写。
原型继承
在传统的基于类的语言中,继承的本质是扩展一个已有的类(class),并生成新的子类(subclass)。
但是,由于JavaScript采用原型继承,我们无法直接扩展一个类,因为根本不存在类。
JavaScript的原型继承方式是:
- 定义新的构造函数,并在内部用
call()
调用希望继承的构造函数,并绑定this
- 借助中间函数
F
实现原型继承,最好通过封装的inherits
函数完成 - 继续在新的构造函数的原型上定义新方法
|
|
class继承
JavaScript的对象模型是基于原型实现的,特点是简单,缺点是理解起来比传统的类-实例模型要困难,最大的缺点是继承的实现需要编写大量代码,并且需要正确实现原型链。
但有更简单的写法。在ES6标准中,新的关键字class
正式被引入到JavaScript中。它的目的就是让定义类更简单。
|
|
用class
定义对象的另一个巨大好处是继承更方便了。不需要写大量的中间对象,直接通过extends
来实现:
|
|
要注意浏览器是不是支持ES6的class。
浏览器
由于JavaScript的出现就是为了能在浏览器中运行,所以,浏览器自然是JavaScript开发者必须要关注的。
目前,主流浏览器分这几种:
- IE 6-11:从IE 10开始支持ES6;
- Chrome:Google出品的基于Webkit内核,内置非常强悍的JS引擎——V8。支持ES6;
- Safari:Mac自带的基于Webkit内核,支持ES6;
- Firefox:Mozilla研制的Gecko内核和JS引擎OdinMonkey。支持ES6;
- 移动设备(IOS/Android):支持ES6
不同的浏览器对JavaScript支持的差异主要是,有些API的接口不一样,比如AJAX,File接口。对于ES6标准,不同的浏览器对各个特性支持也不一样。
在编写JavaScript的时候,就要充分考虑到浏览器的差异,尽量让同一份JavaScript代码能运行在不同的浏览器中。
浏览器对象
JavaScript可以获取浏览器提供的很多对象,并进行操作。
window
window
对象不但充当全局作用域,而且表示浏览器窗口。属性:
innerWidth
:浏览器窗口的内部宽度innerHeight
:内部高度outerWidth
: 浏览器窗口整个宽度outerHeight
: 整个高度
内部宽高是指除去菜单栏、工具栏、边框等占位元素后,用于显示网页的净宽高。
|
|
navigator
navigator
表示浏览器的信息,最常用的属性包括:
appName
:浏览器名称;appVersion
:浏览器版本;language
:浏览器设置的语言;platform
:操作系统类型;userAgent
:浏览器设置的User-Agent字符;
请注意,navigator的信息可以轻易的被用户修改。
|
|
screen
screen
对象表示屏幕的信息,常用的属性有:
width
:屏幕宽度(px);height
:屏幕高度(px);colorDepth
:颜色位数;
|
|
location
location
对象表示当前页面的URL信息。
href
:完整的URLprotocol
:协议host
: 主机名port
:端口pathname
: 路径search
: 参数hash
:assign()
:加载一个新页面reload()
:重载当前页面
|
|
document
document
对象表示当前页面。由于HTML在浏览器中以DOM形式表示为树形结构,document
对象就是整个DOM树的根节点。
title
getElementById()
:按id获得一个DOM节点;getElementsByTagName()
:按tag获得一个DOM节点;getElementsByClassName()
cookie
:为了确保安全,服务器端在设置Cookie时,应该始终坚持使用httpOnly;
|
|
history
history
保存了浏览器的历史记录,JavaScript可以调用history()
对象的back()
或forward()
。
这个对象属于历史遗留问题,任何情况,你都不应该使用history
这个对象。
DOM
DOM(文档对象模型, document object model),是W3C(万维网联盟)的标准。它定义了访问HTML和XML文档的标准。
由于HTML文档被浏览器解析后就是一棵DOM树,要改变HTML的结构,就需要通过JavaScript来操作DOM。
DOM节点有几个操作:
- 更新
- 遍历
- 添加
- 删除
在操作一个DOM节点前,我们需要通过各种方式先拿到这个DOM节点。常用的方法:
document.getElementById()
:由于ID在HTML文档中是唯一的,所以可以直接定位唯一的一个DOM节点。document.getElementsByTagName()
:返回一组DOM节点document.getElementsByClassName()
:要精确地选择DOM,可以先定位父节点,再从父节点开始选择,以缩小范围document.querySelector()
document.queryAelectorAll()
更新DOM
拿到DOM节点后,我们可以对它进行更新。可以直接修改节点文本,方法有两种:
一种是修改innerHTML
属性,这个方式非常强大,不但可以修改一个DOM节点的文本内容,还可以直接通过HTML片段修改DOM节点内部的子树。
用innerHTML时要注意,是否需要写入HTML。如果写入的字符串是通过网络拿到了,要注意对字符编码来避免XSS攻击。
|
|
第二种是修改innerText
或textContent
属性,这样可以自动对字符串进行HTML编码,保证无法设置任何HTML标签。
|
|
两者的区别在于读取属性时,innerText不返回隐藏元素的文本,而textContent返回所有文本。因为CSS允许font-size
这样的名称,但它并非JavaScript有效的属性名,所以需要在JavaScript中改写为驼峰式命名fontSize
。
修改CSS也是经常需要的操作。DOM节点的style
属性对应所有的CSS,可以直接获取或设置。
|
|
插入DOM
如果DOM节点是空的,那么,直接使用innerHTML = <x>aaa</x>
就可以修改DOM节点的内容,相当于插入了新的DOM节点。
如果DOM节点不是空,则不能这样做。有两个新办法。
一个是使用appendChild
:把一个子节点添加到父节点的最后一个子节点。
|
|
|
|
动态创建一个节点,然后田间道DOM树中,可以实现很多功能。
|
|
这个栗子更改了颜色。可在浏览器的console上执行来看效果。
insertBefore
使用parentElement.insertBefore(newElement, referenceElement);
,子节点会插入到referenceElement
之前。
|
|
可见,使用insertBefore
重要的是拿到一个参考子节点的引用。很多时候,需要循环一个父节点的所有字节嗲,可以通过children
属性实现:
|
|
删除DOM
要删除一个节点,首先要获得该节点本身以及它的父节点。然后,调用父节点的removeChild
把自己删掉。
|
|
注意,删除后的节点虽不在文档树中,但其实它还在内存中,可以随时在此被添加到别的位置。
当你遍历一个父节点的子节点并进行删除操作时,要注意,children
属性是一个只读属性,并且它在子节点变化时会实时更新。因此,删除多个节点时,要注意children
属性时刻都在变化。
操作表单
用JavaScript操作表单和操作DOM是类似的,因为表单本身也是DOM树。
不过,表单的输入框、下拉框等可以接收用户输入,所以用JavaScript来操作表单,可以获得用户输入的内容,或者对一个输入框设置新的内容。
HTML表单的输入控件主要有以下几种:
- 文本框:
<input type="text">
; - 口令框:
<input type="password">
; - 单选框:
<input type="radio">
; - 复选框:
<input type="checkbox">
; - 下拉框:
<select>
; - 隐藏文本,用户不可见,但表单提交时会把隐藏文本发送到服务器:
input type="hidden">
。
获取值
如果我们获取了一个<input>
节点的引用,就可以调用value
获得对应的用户输入值。
|
|
这种方式可以用于text, password, hidden, select。但是,对于单选框和复选框,value
属性返回的永远是HTML预设的值,而我们需要获得的实际是用户是否勾上了选项,所以应该用checked
判断。
|
|
设置值
对于text, password, hidden, select,直接设置value
就可以。对于单/复选框,设置checked
为true
或false
即可。
|
|
HTML5控件
HTML5新增了大量标准空间,常用的包括date, datetime, datetime-local, color…,它们都使用<input>
标签。
不支持HTML5的浏览器无法识别新的控件,会把它们当做type="text"
来显示。支持HTML5的浏览器将获得格式化的字符串。
提交表单
最后,JavaScript可以以两种方式来处理表单的提交(AJAX方式在后面介绍)。
一是通过<form>
元素的submit()
方法提交一个表单。这种方式的缺点是扰乱了浏览器对form的正常提交。
|
|
第二种方式是响应<form>
本身的onsubmit
事件,在提交form时作修改:
|
|
注意要return true
来告诉浏览器继续提交,如果return false
,浏览器将不会继续提交form,这种情况通常对应用户输入有误,提示用户错误信息后终止提交form。
在检查和修改<input>
时,要充分利用<input type="hidden">
来传递数据。例如,很多登录表单希望用户输入的口令(出于安全考虑)在提交表单时不传输明文口令,而是口令的MD5。
|
|
这样做看上去没问题,但用户输入了口令提交时,口令框突然会从几个*
变为32个**
(MD5有32个字符)。若不想改变用户的输入,可利用<input type="hidden">
实现:
|
|
没有name
属性的<input>
的数据不会被提交。
操作文件
在HTML表单中,可以上传文件的唯一控件就是<input type="file">
。
注意:当一个表单包含
<input type="file">
时,表单的enctype
必须指定为multipart/form-data
,method
必须指定为post
,浏览器才能正确编码并以multipart/form-data
格式发送表单的数据。
处于安全考虑,浏览器只允许用户点击<input type="file">
来选择本地文件,用JavaScript对<input type="file">
的value
赋值是没有任何效果的。当用户选择了上传某个文件后,JavaScript也无法获得该文件的真实路径。
通常,上传的文件都由后台服务器处理,JavaScript可以在提交表单时对文件的扩展名做检查,以便防止用户上传无效格式的文件。
|
|
File API
由于JavaScript对用户上传的文件操作非常有限,尤其是无法读取文件内容,使得很多需要操作文件的网页不得不用Flash这样的第三方插件来实现。
随着HTML5的普及,新增的File API允许JavaScript读取文件内容,获得更多的文件信息。HTML5的File API提供了File
和FileReader
两个主要对象,可以获得文件信息并读取文件。
回调
在JavaScript中,浏览器的JavaScript执行引擎在执行JavaScript代码时,总是以单线程模式执行,也就是说,任何时候,JavaScript代码都不可能同时有多于1个线程在执行。
你可能会问,单线程模式执行的JavaScript,如何处理多任务?在JavaScript中,执行多任务实际上都是异步调用。因为是异步操作,所以我们在JavaScript代码中就不知道什么时候操作结束,因此需要先设置一个回调函数:
|
|
AJAX
AJAX(Asynchronous JavaScript and XML)不是JavaScript的规范,意思是用JavaScript执行异步网络请求。
如果仔细观察一个Form的提交,你就会发现,一旦用户点击Submit按钮,表单开始提交,浏览器就会刷新页面,然后在新页面里告诉你操作是成功了还是失败了。如果不幸由于网络太慢或者其他原因,就会得到一个404页面。
这就是Web的运作原理:一次HTTP请求对应一个页面。
如果要让用户留在当前页面中,同时发出新的HTTP请求,就必须用JavaScript发送这个新请求,接收到数据后,再用JavaScript更新页面,这样一来,用户就感觉自己仍然停留在当前页面,但是数据却可以不断地更新。
最早大规模使用AJAX的就是Gmail,Gmail的页面在首次加载后,剩下的所有数据都依赖于AJAX来更新。
用JavaScript写一个完整的AJAX代码并不复杂,但是需要注意:AJAX请求是异步执行的,也就是说,要通过回调函数获得响应。
|
|
安全限制
上面代码的URL使用的是相对路径。如果你把它改为http://xxx.com/
,再运行,肯定会报错。在console里,还可以看到错误信息。
这是因为浏览器的同源策略导致的。默认情况下,JavaScript在发送AJAX请求时,URL的域名必须和当前页面完全一致。
那是不是JavaScript无法请求外域(其它网站)的URL了呢?方法还是有的:
- 一是通过Flash插件发送HTTP请求,这种方式可以绕过浏览器的安全限制,但必须安装Flash,并且跟Flash交互。不过Flash用起来麻烦,而且现在用得也越来越少了。
- 二是通过在同源域名下架设一个代理服务器来转发,JavaScript负责把请求发送到代理服务器。代理服务器再把结果返回,这样就遵守了浏览器的同源策略。这种方式麻烦之处在于需要服务器端额外做开发。
- 三是JSONP。它有个限制,只能用GET请求,并且要求返回JavaScript。这种方式跨域实际上是利用了浏览器允许跨域引用JavaScript资源。
CORS
如果浏览器支持HTML5,那么就可以一劳永逸地使用新的跨域策略:CORS(Cross-Origin Resource Sharing),它是HTML5规范定义的如何跨域访问资源。
了解CORS前,我们先搞明白概念。Origin表示本域,也就是浏览器当前页面的域。当JavaScript向外域发起请求后,浏览器收到响应后,首先检查Access-Control-Allow-Origin
是否包含本域,如果是,则此次跨域请求成功,如果不是,则请求失败,JavaScript将无法获取到响应的任何数据。
可见,跨域能否成功,取决于对方服务器是否愿意给你设置一个正确的Access-Control-Allow-Origin
,决定权始终在对方手中。
Promise
在JavaScript的世界中,所有代码都是单线程执行的。由于这个缺陷,导致JavaScript的所有网络操作,浏览器事件,都必须是异步执行。
异步执行可以用回调函数实现:
|
|
可见,异步操作会在将来某个时间点触发一个函数调用。
Promise有各种开源实现,在ES6中被统一规范,由浏览器直接支持。
Promise最大的好处是在异步执行的流程中,把执行代码和处理结果的代码清洗地分离。
Promise还可以做更多的事情,比如,有若干个异步任务,需要先做任务1,如果成功后再做任务2,任何任务失败则不再继续并执行错误处理函数。 要串行执行这样的异步任务,不用Promise需要些一层一层的嵌套代码。
|
|
除了串行执行若干异步任务外,Promise还可以并行执行异步任务。
如果我们组合使用Promise,就可以把很多异步任务以并行和串行的方式组合起来执行。
Canvas
Canvas是HTML5新增的组件,它就像一块幕布,可以用JavaScript在上面绘制各种图标、动画等。没有Canvas的年代,绘图只能借助Flash插件实现,页面不得不用JavaScript和Flash进行交互。有了Canvas,我们就再也不需要Flash了,直接使用JavaScript完成绘制。
一个Canvas定义了一个指定尺寸的矩形框,在这个范围内我们可以随意绘制:
|
|
绘制形状
我们可以在Canvas上绘制各种形状。在绘制前,我们需要了解以下Canvas的坐标系统。Canvas的坐标以左上角为原点,水平向右为X轴,垂直向下为Y轴,以像素为单位,所以每个点都是非负整数。
绘制文本
绘制文本就是在指定的位置输出文本,可以设置文本的字体、样式、阴影等,与CSS完全一致。
Canvas除了能绘制基本的形状和文本,还可以实现动画、缩放、各种滤镜和像素转换等高级操作。如果要实现非常复杂的操作,考虑以下优化方案:
- 通过创建一个不可见的Canvas来绘图,然后将最终绘制结果复制到页面的可见Canvas中;
- 尽量使用整数坐标而不是浮点数;
- 可以创建多个重叠的Canvas绘制不同的层,而不是在一个Canvas中绘制非常复杂的图;
- 背景图片如果不变可以直接用
<img>
标签并放到最底层。
JQuery
你可能听说过jQuery,它名字起得很土,但却是JavaScript世界中使用最广泛的一个库。 江湖传言,全世界大约有80~90%的网站直接或间接地使用了jQuery。鉴于它如此流行,又如此好用,所以每一个入门JavaScript的前端工程师都应该了解和学习它。
JQuery的理念是Write Less, Do More,让你写更少的代码,完成更多的工作。
JQuery能帮助解决一些很重要的问题:
- 消除浏览器差异
- 简洁的操作DOM的方法:写
$('#test')
肯定比document.getElementById('test')
来的简洁 - 轻松实现动画、修改CSS等各种操作。
JQuery版本
JQuery有1.x和2.x两个主要版本,区别在于2.x移除了对古老的IE6、7、8的支持,因此2.x的代码更精简。
JQuery只是一个jquery-xxx.js
文件,但你会看到有compressed(已压缩)和uncompressed(未压缩)两种版本,使用时完全一样,但如果你想深入研究jQuery源码,那就用uncompressed版本。
使用JQuery
使用JQuery只需要在页面的<head>
引入JQuery文件即可:
|
|
$符号
$
符号是著名的JQuery符号。实际上,Jquery把所有功能全部封装在一个全局变量JQuery
中,而$
也是一个合法的变量名,它是JQuery
的别名。
|
|
$
本质上是一个函数,但函数也是对象。于是$
除了可以直接调用外,也可以有很多其它属性。
注意:你看到的
$
函数名可能不是JQuery(selector, context)
,因为很多JavaScript压缩工具可以对函数名和参数改名,所以压缩过的JQuery源码$
函数可能变成a(b, c)
。
绝大多数时候,我们都直接用$
。但是,如果$
这个变量不幸地被占用了,而且还不能改,那我们只能让JQuery
把$
变量交出来,然后就只能使用JQuery
这个变量。
|
|
这种黑魔法的原理是JQuery在占用$
之前,现在内部保存了原来的$
,调用JQuery.noConflict()
时会把原来保存的变量还原。
选择器
选择器是JQuery的核心,一个选择器写出来大概是这样:$('#dom-id')
。
为什么JQuery要发明选择器?来回顾一下DOM操作中经常使用的代码:
|
|
这些代码实在太过繁琐。并且在层级关系中,很多时候需要递归查找所有子节点。 JQuery的选择器就是帮助我们快速定位到一个或多个DOM节点。
按id查找
|
|
它返回JQuery对象。JQuery对象类似数组,它的每个元素都是一个引用了DOM节点的对象。
JQuery的选择器不会返回undefined
或null
,这样的好处是不必在下一行判断if (div === undefined)
。
|
|
通常情况下你不需要获取DOM对象,直接使用jQuery对象更加方便。如果你拿到了一个DOM对象,那可以简单地调用$(aDomObject)把它变成jQuery对象,这样就可以方便地使用jQuery的API了。
按tag查找
|
|
按class查找
|
|
按属性查找
|
|
组合查找
组合查找就是把上述简单选择器组合起来使用。
|
|
多项选择器
多项选择器就是把多个选择器用逗号,
组合起来:
|
|
层级选择器
除了基本的选择器外,jQuery的层级选择器更加灵活,也更强大。因为DOM的结构就是层级结构,所以我们经常要根据层级关系进行选择。
层级选择器(Descendant Selector)
如果两个DOM元素具有层级关系,就可以用$('ancestor descendant')
来选择,层级之间用空格隔开。
|
|
这种层级选择器相比单个的选择器好处在于,它缩小了选择范围,因为首先要定位父节点,才能选择相应的子节点,这样避免了页面其他不相关的元素。
子选择器(Child Selector)
子选择器$('parent>child')
是限定了父子关系的层级选择器。
|
|
过滤器(Filter)
过滤器一般不单独使用,它通常附加在选择器上,帮助我们更精确地定位元素。
|
|
表单相关
针对表单元素,jQuery还有一组特殊的选择器:
:input
,可选择input, textarea, select, button:file
,可选择<input type="file">
,和input[type=file]
一样:checkbox
:radio
:focus
:checked
:enabled
:disabled
查找和过滤
通常情况下选择器可以直接定位到我们想要的元素,但是,当我们拿到一个jQuery对象后,还可以以这个对象为基准,进行查找和过滤。
|
|
|
|
操作DOM
jQuery的选择器很强大,用起来又简单又灵活,但是搞了这么久,我拿到了jQuery对象,到底要干什么?
当然是操作对应的DOM节点啦!
回顾一下修改DOM的CSS、文本、设置HTML有多么麻烦,而且有的浏览器只有innerHTML,有的浏览器支持innerText,有了jQuery对象,不需要考虑浏览器差异了,全部统一操作!
修改Text和HTML
jQuery对象的text()
和html()
方法分别获取节点的文本和原始HTML文本。
|
|
修改CSS
|
|
显示和隐藏DOM
考虑到显示和隐藏DOM元素使用非常普遍,jQuery直接提供show()
和hide()
方法。
注意,隐藏DOM节点并未改变DOM树的结构,它只影响DOM节点的显示。这和删除DOM节点是不同的。
|
|
获取DOM信息
利用jQuery对象的若干方法,我们直接可以获取DOM的许多信息,而无需针对不同浏览器编写特定代码。
width()
height()
attr()
:获取或修改属性removeAttr()
prop()
- …
|
|
操作表单
对于表单元素,jQuery对象统一提供val()
方法获取和设置对应的value
属性。一个val()
就统一了各种输入框的取值和赋值的问题。
|
|
修改DOM结构
有了jQuery,我们就专注于操作jQuery对象本身,底层的DOM操作由jQuery完成就可以了,这样一来,修改DOM也大大简化了。
添加DOM
除了html()
这种暴力方法外,还可以用append()
方法。append()
把DOM添加到最后,prepend()
则把DOM添加到最前。
|
|
除了接受字符串,append()
还可以传入原始的DOM对象、jQuery对象和函数对象。
|
|
同级节点可以用after()
或者before()
方法。
删除节点
要删除DOM节点,拿到jQuery对象后直接调用remove()
方法就可以了。如果jQuery对象包含若干DOM节点,实际上可以一次删除多个DOM节点。
事件
因为JavaScript在浏览器中以单线程模式运行,页面加载后,一旦页面上所有的JavaScript代码被执行完后,就只能依赖触发事件来执行JavaScript代码。
浏览器在接收到用户的鼠标或键盘输入后,会自动在对应的DOM节点上触发相应的事件。如果该节点已经绑定了对应的JavaScript处理函数,该函数就会自动调用。由于不同的浏览器绑定事件的代码都不太一样,所以用jQuery来写代码,就屏蔽了不同浏览器的差异,我们总是编写相同的代码。
|
|
jQuery能够绑定的事件主要有:
- 鼠标事件
- click:鼠标单击时触发;
- dblclick:鼠标双击时触发;
- mouseenter:鼠标进入时触发;
- mouseleave:鼠标移出时触发;
- mousemove:鼠标在DOM内部移动时触发;
- hover:鼠标进入和退出时触发两个函数(相当于mouseenter+mouseleave)。
- 键盘事件:仅作用在当前焦点的DOM上
- keydown:键盘按下时触发;
- keyup:键盘松开时触发;
- keypress:按一次键后触发。
- 其它事件
- focus:当DOM获得焦点时触发;
- blur:当DOM失去焦点时触发;
- change:当input, select, textarea的内容改变时触发;
- submit:当form提交时触发;
- ready:当页面被载入并且DOM树完成初始化后触发。
取消绑定
一个已被绑定的事件可以解除绑定,通过off('click', function)
实现。
|
|
需要特别注意,以下这种写法无效:
|
|
这是因为两个匿名函数虽然长得一模一样,但是它们是两个不同的函数对象,off('click', function () {...})
无法移除已绑定的第一个匿名函数。
为了实现移除效果,可以使用off('click')
一次性移除已绑定的click
事件的所有处理函数。
同理,无参数调用off()
一次性移除已绑定的所有类型的事件处理函数。
事件触发条件
一个需要注意的问题是,事件的触发总是由用户操作引发的。
浏览器安全限制
在浏览器中,有些JavaScript代码只有在用户触发下才能执行。
|
|
动画
用JavaScript实现动画,原理非常简单:我们只需要以固定的时间间隔(例如,0.1秒),每次把DOM元素的CSS样式修改一点(例如,高宽各增加10%),看起来就像动画了。
但是要用JavaScript手动实现动画效果,需要编写非常复杂的代码。如果想要把动画效果用函数封装起来便于复用,那考虑的事情就更多了。
使用jQuery实现动画,代码就非常简单了。
jQuery内置的几种动画样式
show()
:显示DOM元素,从左上角展开;hiden()
:隐藏DOM元素,从左上角收缩;sideUp()
:在垂直方向展开;sideDown()
:在垂直反向收缩;fadeIn()
:动画效果淡入;fadeOut()
:动画效果淡出。
|
|
自定义动画
使用animate()
可实现任意动画效果,需要传入的参数就是DOM元素最终的CSS状态和时间,jQuery在时间段内不断调整CSS直到达到设定的值。
|
|
串行动画
jQuery的动画效果还可以串行执行,通过delay()
方法还可以实现暂停,这样我们可以实现更复杂的动画效果。
|
|
AJAX
jQuery在全局对象jQuery
(也就是$
)绑定了ajax()
函数,可以处理AJAX请求。ajax(url, settings)
常用的选项如下:
- async:是否异步执行AJAX请求,默认true,千万不要指定为false;
- method:缺省为GET;
- content type:发送POST请求的格式,默认为
application/x-www-form-urlencoded; charset=UTF-8
,也可指定为text/plain
,application/json
; - data:发送的数据,可以是字符串、数组、对象;
- headers:发送的额外HTTP头,必须是一个对象;
- dataType:接收的数据格式,可指定为html, xml, json, text等。
get
对常用的AJAX操作,jQuery提供了一些辅助方法。由于GET请求最常见,所以jQuery提供了get()
方法。
|
|
post
与get类似,但传入的第二个参数默认被序列化为application/x-www-form-urlencoded
。
|
|
getJSON
由于json越来越普遍,所以jQuery也提供了getJSON()
方法来快速通过GET获取一个json对象。
|
|
安全限制
jQuery的AJAX完全封装的是JavaScript的AJAX操作,所以它的安全限制和前面讲的用JavaScript写AJAX完全一样。
扩展
当我们使用jQuery对象的方法时,由于jQuery对象可以操作一组DOM,而且支持链式操作,所以用起来非常方便。但是jQuery内置的方法永远不可能满足所有的需求。
我们可以扩展jQuery来实现自定义方法。我们可以扩展jQuery来实现自定义方法。
编写jQuery插件
给jQuery对象绑定一个新方法是通过扩展$.fn
对象实现的。
|
|
另一种方法是使用jQuery提供的辅助方法$.extend(target, obj1, obj2, ...)
,它把多个对象的属性合并到第一个target对象中,遇到同名属性,总是使用靠后的对象的值,也就是越往后优先级越高。
紧接着用户对highlight2()
提出了意见:每次调用都需要传入自定义的设置,能不能让我自己设定一个缺省值,以后的调用统一使用无参数的highlight2()
?也就是说,我们设定的默认值应该能允许用户修改。那默认值放哪比较合适?放全局变量肯定不合适,最佳地点是$.fn.highlight2
这个函数对象本身。
|
|
最终,我们得出编写一个jQuery插件的原则:
- 给
$.fn
绑定函数,实现插件的代码逻辑; - 插件函数最后要
return this;
以支持链式调用; - 插件函数要有默认值,绑定在
$.fn.<pluginName>.defaults
上; - 用户在调用时可传入设定值以便覆盖默认值。
针对特定元素的扩展
我们还知道jQuery对象的有些方法只能作用在特定的DOM元素上,比如submit()
方法只能针对form
。如果我们编写的扩展只能针对某些类型的DOM元素,应该怎么写?
还记得jQuery的选择器支持filter()
方法来过滤吗?我们可以借助这个方法来实现针对特定元素的扩展。
错误处理
错误分两种:
- 一种是程序逻辑写的不对,导致代码执行异常;
- 一种是执行过程中,程序可能遇到无法预测的异常情况而报错。
错误处理是程序设计时必须要考虑的问题。
|
|
错误类型
JavaScript有一个标准的Error
对象表示错误,还有从Error
派生的TypeError
, FeferenceError
等错误对象。
|
|
抛出错误
程序也可以主动抛出一个错误,让执行流程直接跳转到catch
块。抛出错误使用throw
语句。
错误传播
如果代码发生了错误,有没有被try...catch
捕获,那么程序执行流程会跳转到哪呢?
如果在一个函数内部发生了错误,它自身没有捕获,错误就会被抛到外层调用函数,如果外层函数也没有捕获,该错误会一直沿着函数调用链向上抛出,直到被JavaScript引擎捕获,代码终止执行。
异步错误处理
编写JavaScript代码时,我们要时刻牢记,JavaScript引擎是一个事件驱动的执行引擎,代码总是以单线程执行,而回调函数的执行需要等到下一个满足条件的事件出现后,才会被执行。
所以,涉及到异步代码,无法在调用时捕获,原因就是在捕获的当时,回调函数并未执行。类似的,当我们处理一个事件时,在绑定事件的代码处,无法捕获事件处理函数的错误。
underscore
正如jQuery统一了不同浏览器之间的DOM操作的差异,让我们可以简单地对DOM进行操作,underscore则提供了一套完善的函数式编程的接口,让我们更方便地在JavaScript中实现函数式编程。
jQuery在加载时,会把自身绑定到唯一的全局变量$
上,underscore与其类似,会把自身绑定到唯一的全局变量_
上,这也是为啥它的名字叫underscore的原因。
Node.js
从本章开始,我们就正式开启JavaScript的后端开发之旅。
Node.js是目前非常火热的技术,但是它的诞生经历却很奇特。
Google认为要运行现代Web应用,浏览器必须有一个性能非常强劲的JavaScript引擎,于是Google自己开发了一个高性能JavaScript引擎,名字叫V8,以BSD许可证开源。
话说有个叫Ryan Dahl的歪果仁,他的工作是用C/C++写高性能Web服务。对于高性能,异步IO、事件驱动是基本原则,但是用C/C++写就太痛苦了。于是这位仁兄开始设想用高级语言开发Web服务。他评估了很多种高级语言,发现很多语言虽然同时提供了同步IO和异步IO,但是开发人员一旦用了同步IO,他们就再也懒得写异步IO了,所以,最终,Ryan瞄向了JavaScript。
因为JavaScript是单线程执行,根本不能进行同步IO操作,所以,JavaScript的这一缺陷导致了它只能使用异步IO。
选定了开发语言,还要有运行时引擎。这位仁兄曾考虑过自己写一个,不过明智地放弃了,因为V8就是开源的JavaScript引擎。让Google投资去优化V8,咱只负责改造一下拿来用,还不用付钱,这个买卖很划算。
于是在2009年,Ryan正式推出了基于JavaScript语言和V8引擎的开源Web服务器项目,命名为Node.js。虽然名字很土,但是,Node第一次把JavaScript带入到后端服务器开发,加上世界上已经有无数的JavaScript开发人员,所以Node一下子就火了起来。
在Node上运行的JavaScript相比其他后端开发语言有何优势?最大的优势是借助JavaScript天生的事件驱动机制加V8高性能引擎,使编写高性能Web服务轻而易举。
其次,JavaScript语言本身是完善的函数式语言,在前端开发时,开发人员往往写得比较随意,让人感觉JavaScript就是个“玩具语言”。但是,在Node环境下,通过模块化的JavaScript代码,加上函数式编程,并且无需考虑浏览器兼容性问题,直接使用最新的ECMAScript 6标准,可以完全满足工程上的需求。
Node.js是一个开源和跨平台的JavaScript runtime environment。
安装Node.js和npm
由于Node.js平台是在后端运行JavaScript代码,所以需要在本机按照Node环境。
安装Node.js
详情请看官网文档。
npm
npm is the standard package manager for Node.js.
npm其实是Node.js的包管理工具。 为啥我们需要一个包管理工具呢?因为我们在Node.js上开发时,会用到很多别人写的JavaScript代码。如果我们要使用别人写的某个包,每次都根据名称搜索一下官方网站,下载代码,解压,再使用,非常繁琐。于是一个集中管理的工具应运而生:大家都把自己开发的模块打包后放到npm官网上,如果要使用,直接通过npm安装就可以直接用,不用管代码存在哪,应该从哪下载。
更重要的是,如果我们要使用模块A,而模块A又依赖于模块B,模块B又依赖于模块X和模块Y,npm可以根据依赖关系,把所有依赖的包都下载下来并管理起来。否则,靠我们自己手动管理,肯定既麻烦又容易出错。
讲了这么多,npm究竟在哪?其实npm已经在Node.js安装的时候顺带装好了。
第一个Node程序
在前面的章节中,编写的JavaScript代码都是在浏览器中运行的,因此,我们可以直接在浏览器中敲代码,然后直接运行。
从本章开始,我们编写的JavaScript代码将不能在浏览器环境中执行了,而是在Node环境中执行。
|
|
执行:
|
|
严格模式
在服务器环境下,如果有很多JavaScript文件,每个文件都写上'use strict';
很麻烦。我们可以给Nodejs传递一个参数,让Node直接为所有js文件开启严格模式:
|
|
模块
为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很多编程语言都采用这种组织代码的方式。在Node环境中,一个.js
文件就称之为一个模块(module)。
使用模块有什么好处?最大的好处是大大提高了代码的可维护性。其次,编写代码不必从零开始。当一个模块编写完毕,就可以被其他地方引用。我们在编写程序的时候,也经常引用其他模块,包括Node内置的模块和来自第三方的模块。
|
|
|
|
CommonJS规范
这种模块加载机制被称为CommonJS规范。在这个规范下,每个.js
文件都是一个模块,它们内部各自使用的变量名和函数名都互不冲突。
一个模块想要对外暴露变量(函数也是变量),可以用module.exports = variable;
,一个模块要引用其他模块暴露的变量,用var ref = require('module_name');
就拿到了引用模块的变量。