为什么要重新介绍 JavaScript?
因为 JavaScript 被误解为世界上最不懂编程的语言。 经常有人嘲笑 JavaScript 是不值钱的小玩具,但是在其简单的外表下面是强大的语言特性。 JavaScript 现在已被用于无数的高知名度应用程序,充分表明这项技术的深层知识是一项开发者需要具备的重要技能。
首先了解一下这门语言的历史很有用。
脚本语言(Scripting language)是一种新兴的计算机编程语言,它的原则是用简洁而又快速的方式去完成。 脚本语言又被称为扩建的语言,或者动态语言,是一种编程语言,用来控制软件应用程序,脚本通常以文本(如ASCII)保存,只在被调用时进行解释或编译。 最常用的 JavaScript 解释器是浏览器,但是在其他许多地方都有 JavaScript 解释器,如 Adobe Acrobat, Photoshop, SVG 图像, 雅虎的 Widget 引擎, 服务器端环境 Node.js, 还有 NoSQL 数据库(如开源的 Apache CouchDB), 嵌入式计算机, 桌面环境(如GNOME)等等。
概览
JavaScript是一种多范式的动态语言,包含类型和运算符,标准内置对象和方法。
数据类型
JavaScript 的数据类型
Number 数值:双精度64位,没有整型,所以要注意算术运算:
0.1 + 0.2 == 0.30000000000000004;
可以使用内置的 parseInt() 函数把字符串转化为整数 String 字符串 Boolean 布尔值 Symbol (new in ES2015) 元件 Object 对象
null 空值 undefined 未定义
3. Number 数值
和其他编程语言(如 C 和 Java)不同,JavaScript 不区分整数值和浮点数值,所有数字在 JavaScript 中均用浮点数值表示。 你可以使用内置函数 parseInt() 将字符串转换为整型。该函数的第二个参数表示字符串所表示数字的基(进制)。
parseInt ( "123" , 10 ) ;
parseInt ( "010" , 10 ) ;
如果想把一个二进制数字字符串转换成整数值,只要把第二个参数设置为 2 就可以了
parseInt ( "11" , 2 ) ;
JavaScript 还有一个类似的内置函数 parseFloat(),用以解析浮点数字符串,与parseInt()不同的地方是,parseFloat()只应用于解析十进制数字。 单元运算符 + 也可以把数字字符串转换成数值。 如果给定的字符串不存在数值形式,函数会返回一个特殊的值 NaN(Not a Number 的缩写)。 要小心NaN:如果把 NaN 作为参数进行任何数学运算,结果也会是 NaN。
parseInt ( "hello" , 10 ) ;
NaN + 5 ;
JavaScript 还有两个特殊值:Infinity(正无穷)和 -Infinity(负无穷): parseInt() 和 parseFloat() 函数会尝试逐个解析字符串中的字符,直到遇上一个无法被解析成数字的字符,然后返回该字符前所有数字字符组成的数字。使用运算符 “+” 将字符串转换成数字,只要字符串中含有无法被解析成数字的字符,该字符串都将被转换成 NaN。请你用这两种方法分别解析“10.2abc”这一字符串,比较得到的结果,理解这两种方法的区别。
其他数据类型
String 字符串
JavaScript 中的字符串是一串UTF-16编码单元的序列,每一个编码单元由一个 16 位二进制数表示。 字符串也有methods(方法)能让你操作字符串和获取字符串的信息。
"hello" . charAt ( 0 ) ;
"hello, world" . replace ( "hello" , "goodbye" ) ;
"hello" . toUpperCase ( ) ;
null 和 undefined 截然不同
null 表示一个空值(non-value),必须使用null关键字才能访问。 undefined 是“undefined(未定义)”类型的对象,表示一个未初始化的值,也就是还没有被分配的值。 undefined 实际上是一个常数。null 则是空值。
Boolean 布尔值,任何值可以转化为布尔值,根据下面规则:
false、0、空字符串("")、NaN、null 和 undefined 被转换为 false 所有其他值被转换为 true JavaScript 会在需要一个布尔变量时隐式完成类型转换(比如在 if 条件语句中)。
7. 变量
声明变量使用 3 个关键字:let, const, var. let 声明的变量是一个块级变量,作用域只在块内。 var 是最常用的变量声明关键字。是 JavaScript 原来就有的唯一的声明变量的关键字。let 和 const 都是后来新增的。 声明变量后而不赋值,则其值是 undefined 。
8. 运算符
9. 控制结构
除了顺序、分支、循环,有特殊结构吗?有 特殊循环 for…of :for value of 数组 特殊循环 for…in :for key in object && 和 || 运算符使用短路逻辑(short-circuit logic),也就是说后面的运算是否执行,取决于前面的运算。这对于检查 null 对象很有用。
actionscript 就是这样,我们经常使用这个特性。
var name = (o && o.getName()); //对象存在,则访问其属性
var name = (cachedName || (cachedName = getName()));//值不存在,才获取 三元运算符:条件 ? 真则取值 : 假则取值
var allowed = (age > 18) ? ‘yes’ : ‘no’; switch 和 case 中可以使用表达式,它们之间的比较是 === 运算符:
10. Objects 对象
function Person ( name, age) {
this . name = name;
this . age = age;
}
var you = new Person ( 'ming' , 30 ) ;
Arrays 数组
注意 array.length 不一定是数组中元素的实际个数,例如:
var a = [ 'dog' , 'cat' , 'hen' ] ;
a[ 100 ] = 'fox' ;
a. length;
如果查询一个不存在的索引,则得到 undefined 例如:
a = [ 'a' , 'b' , 'c' ] ;
console. log ( "a=" , a, "a[1]=" , a[ 1 ] , "a[11]=" , a[ 11 ] ) ;
迭代数组的另一种方法是 ECMAScript5 添加的 forEach():
[ 'dog' , 'cat' , 'hen' ] . forEach ( function ( currentValue, index, array) {
} ) ;
12. 函数和对象:是语言的核心组件
调用函数,可以不传递需要的参数。也可以传递多余的参数。这看起来有点傻。 函数可以接受任意数量的参数。使用 arguments 对象, rest 语法 …args,
…args(包括省略号)叫作剩余参数(rest arguments)。如名所示,这个东西包含了剩下的参数。 使用函数的 apply() 方法调用函数. 使用展开运算符 spread syntax 传递一个数组给函数。 JavaScript 允许你创建匿名函数: JavaScript 允许以递归方式调用函数。递归在处理树形结构(比如浏览器 DOM)时非常有用。
function countChars ( elm) {
if ( elm. nodeType == 3 ) {
return elm. nodeValue. length;
}
var count = 0 ;
for ( var i = 0 , child; child = elm. childNodes[ i] ; i++ ) {
count += countChars ( child) ;
}
return count;
}
需要注意的是 JavaScript 函数本身是个对象——你可以给它们添加属性或者更改它们的属性,这与前面的对象部分一样。
13. 自定义对象
在经典的面向对象语言中,对象是指数据和在这些数据上进行的操作的集合。 JavaScript 是一种基于原型的编程语言,并没有 class 语句,而是把函数用作类。 关键字 this。当使用在函数中时,this 指代当前的对象,也就是调用了函数的对象。
如果在一个对象上使用点或者方括号来访问属性或方法,这个对象就成了 this。例如:a.fullName(), a[‘fullName’]
如果并没有使用“点”运算符调用某个对象,那么 this 将指向全局对象(global object)。
这是一个经常出错的地方。在 ActionScript3 中经常会遇到。
function makePerson ( first, last) {
return {
first: first,
last: last,
fullName: function ( ) {
return this . first + ' ' + this . last;
} ,
fullNameReversed: function ( ) {
return this . last + ', ' + this . first;
}
}
}
s = makePerson ( "Simon" , "Willison" ) ;
s. fullName ( ) ;
s. fullNameReversed ( ) ;
var fullName = s. fullName;
fullName ( ) ;
new 关键字将生成的 this 对象返回给调用方,而被 new 调用的函数成为构造函数。 prototype 是一个名叫原型链(prototype chain)的查询链的一部分:当你试图访问一个没有定义的属性时,解释器会首先检查这个 prototype 来判断是否存在这样一个属性。
function Person ( first, last) {
this . first = first;
this . last = last;
}
Person. prototype. fullName = function ( ) {
return this . first + ' ' + this . last;
}
Person. prototype. fullNameReversed = function ( ) {
return this . last + ', ' + this . first;
}
JavaScript 允许你在程序中的任何时候修改原型(prototype)中的一些东西,也就是说你可以在运行时(runtime)给已存在的对象添加额外的方法。
s = new Person ( "Simon" , "Willison" ) ;
s. firstNameCaps ( ) ;
Person. prototype. firstNameCaps = function ( ) {
return this . first. toUpperCase ( )
}
s. firstNameCaps ( ) ;
有趣的是,你还可以给 JavaScript 的内置函数原型(prototype)添加东西。
var s = "Simon" ;
s. reversed ( ) ;
String. prototype. reversed = function ( ) {
var r = "" ;
for ( var i = this . length - 1 ; i >= 0 ; i-- ) {
r += this [ i] ;
}
return r;
}
s. reversed ( ) ;
原型(prototype)组成链的一部分。这条链的根节点是 Object.prototype,它包括 toString() 方法,这对于调试我们的自定义对象很有用。
var s = new Person ( "Simon" , "Willison" ) ;
s;
Person. prototype. toString = function ( ) {
return '<Person: ' + this . fullName ( ) + '>' ;
}
s. toString ( ) ;
apply() 中的第一个参数应该是一个被当作 this 来看待的对象。 apply() 有一个姐妹函数,名叫 call,它也可以允许你设置 this,但它带有一个扩展的参数列表而不是一个数组。
function lastNameCaps ( ) {
return this . last. toUpperCase ( ) ;
}
var s = new Person ( "Simon" , "Willison" ) ;
lastNameCaps. call ( s) ;
s. lastNameCaps = lastNameCaps;
s. lastNameCaps ( ) ;
14. 内部函数
关于 JavaScript 中的嵌套函数,一个很重要的细节是它们可以访问父函数作用域中的变量。
function betterExampleNeeded ( ) {
var a = 1 ;
function oneMoreThanA ( ) {
return a + 1 ;
}
return oneMoreThanA ( ) ;
}
如果某个函数依赖于(需要调用)另外一两个函数,而那一两个函数对你其余的代码没有用处(不用),你可以将它们嵌套在这个函数内部,这样可以减少全局作用域下的函数数量,有利于编写易于维护的代码。 这也是一个减少使用全局变量的好方法。
当编写复杂代码时,程序员往往试图使用全局变量,将值共享给多个函数,但这样做会使代码很难维护。
内部函数可以共享父函数的变量,所以你可以使用这个特性把一些函数捆绑在一起,这样可以有效地防止“污染”你的全局命名空间——你可以称它为“局部全局(local global)”。
虽然这种方法应该谨慎使用,但它确实很有用,应该掌握。 嵌套函数有利于封装细节,简化代码。全局函数更少,全局变量更少。
15. 闭包:JavaScript 中必须提到的功能最强大的抽象概念之一
function makeAdder ( a) {
return function ( b) {
return a + b;
}
}
a = makeAdder ( 5 ) ;
b = makeAdder ( 20 ) ;
c = a ( 6 ) ;
let d = b ( 7 ) ;
每当 JavaScript 执行一个函数时,都会创建一个作用域对象(scope object),用来保存在这个函数中创建的局部变量。这与那些保存了所有全局变量和函数的全局对象(global object)类似,但仍有一些很重要的区别:
第一,每次函数被执行的时候,就会创建一个新的、特定的作用域对象;
第二,不能从 JavaScript 代码中直接访问作用域对象,也没有可以遍历当前的作用域对象里面属性的方法。 通常 JavaScript 的垃圾回收器会在函数返回时回收该函数创建的作用域对象。但是返回的函数却保留一个指向那个作用域对象的引用。结果是这个作用域对象不会被垃圾回收器回收,直到指向该函数返回的那个函数对象的引用计数为零。 作用域对象组成了一个名为作用域链(scope chain)的链。 一个闭包 就是一个函数和被创建的函数中的作用域对象的组合。
内存泄露
使用闭包的一个坏处是,在 IE 浏览器中它会很容易导致内存泄露。
JavaScript 是一种具有垃圾回收机制的语言——对象在被创建的时候分配内存,然后当指向这个对象的引用计数为零时,浏览器会回收内存。
IE 浏览器有自己的一套垃圾回收机制,这套机制与 JavaScript 提供的垃圾回收机制进行交互时,可能会发生内存泄露。
在 IE 中,每当在一个 JavaScript 对象和一个本地对象之间形成循环引用时,就会发生内存泄露。
不过一般也很少发生如此明显的内存泄露现象——通常泄露的数据结构有多层的引用(references),往往掩盖了循环引用的情况。
function addHandler ( ) {
var el = document. getElementById ( 'el' ) ;
el. onclick = function ( ) {
el. style. backgroundColor = 'red' ;
}
}
这段代码会发生内存泄露。为什么?因为对 el 的引用不小心被放在一个匿名内部函数中。这就在 JavaScript 对象(这个内部函数)和本地对象之间(el)创建了一个循环引用。
这个问题有很多种解决方法,最简单的一种是不要使用 el 变量:
function addHandler ( ) {
document. getElementById ( 'el' ) . onclick = function ( ) {
this . style. backgroundColor = 'red' ;
} ;
}
另外一种避免闭包的好方法是在 window.onunload 事件发生期间破坏循环引用。很多事件库都能完成这项工作。
注意这样做将使 Firefox 中的 bfcache 无法工作。所以除非有其他必要的原因,最好不要在 Firefox 中注册一个onunload 的监听器。
结束