目录
JS:嵌入在网页中的编程语言,也称为“脚本(script)语言”,负责编写网页交互操作
Javascript
HTML+CSS+JS属于前端三剑客
HTML:基于标签编写网页文档,版本是HTML5,简称H5
CSS:基于选择器和属性美化,布局排版网页,版本是CSS3
JS:嵌入在网页中的编程语言,也称为“脚本(script)语言”,负责编写网页交互操作
变量,流程控制,对象,数组,函数
Java和Javascript的区别
1.Java是强类型语言,静态类型语言
建一个 01.JS的数据类型 文件
建一个 02.函数作为参数 文件
建一个 03.函数作为返回值 文件
建一个 04.回调函数 文件
建一个 05.立即执行函数 文件
建一个 06外部js文件和 01.js 02.js
建一个 07.数组的使用 文件
建一个 08.创建对象的几种方式 文件
JS的继承依靠是原型链
1.同一个构造方法构造出来的对象拥有一个__proto__指针,指向同一个原型对象
2.构造方法身上有一个prototype指针,与该构造方法构造出来的所有对象的_proto_指向同一个原型对象
3.通过"指针._proto_.成员"或者构造方法.prototype.成员"可以设置一个所有该构造方法构造出来的对象都可以继承的成员
4.通过"Object.prototype.成员"设置一个任意构造方法构造出来的对象都可以继承的成员
面试题1:什么是原型对象,什么是对象原型
.构造方法身上有一个prototype指针,与该构造方法构造出来的所有对象身上都有一个_proto_指针,指向同一个对象,这个对象称为原型对象
面试题2:什么是原型链
所有的对象和构造方法身上都有指针指向一个原型对象,原型对象身上有指针指向其继承的构造函数的原型对象,最顶层是object的原型对象,这种由子构造函数及其父辈构造函数的原型对象组成单向链表称为原型链,在对象身上访问成员的时候,会沿着原型链一直向上追溯,如果追溯到顶层object的原型对象还是找不到,返回undefined
建一个 09-定时器 文件
建一个 10-数组的高阶函数 文件
基本语法
JavaScript的语法和Java语言类似,每个语句以;
结束,语句块用{...}
。但是,JavaScript并不强制要求在每个语句的结尾加;
,浏览器中负责执行JavaScript代码的引擎会自动在每个语句的结尾补上;
。
注意花括号{...}
内的语句具有缩进,通常是4个空格。缩进不是JavaScript语法要求必须的,但缩进有助于我们理解代码的层次,所以编写代码时要遵守缩进规则。很多文本编辑器具有“自动缩进”的功能,可以帮助整理代码。
JavaScript本身对嵌套的层级没有限制,但是过多的嵌套无疑会大大增加看懂代码的难度。遇到这种情况,需要把部分代码抽出来,作为函数来调用,这样可以减少代码的复杂度。
数据类型和变量
数据类型
计算机顾名思义就是可以做数学计算的机器,因此,计算机程序理所当然地可以处理各种数值。但是,计算机能处理的远不止数值,还可以处理文本、图形、音频、视频、网页等各种各样的数据,不同的数据,需要定义不同的数据类型。在JavaScript中定义了以下几种数据类型:
计算机由于使用二进制,所以,有时候用十六进制表示整数比较方便,十六进制用0x前缀和0-9,a-f表示,例如:0xff00
,0xa5b4c3d2
,等等,它们和十进制表示的数值完全一样。
注意%
是求余运算。
字符串
字符串是以单引号'或双引号"括起来的任意文本,比如'abc'
,"xyz"
等等。请注意,''
或""
本身只是一种表示方式,不是字符串的一部分,因此,字符串'abc'
只有a
,b
,c
这3个字符。
布尔值
布尔值和布尔代数的表示完全一致,一个布尔值只有true
、false
两种值,要么是true
,要么是false
,可以直接用true
、false
表示布尔值,也可以通过布尔运算计算出来:
null和undefined
null
表示一个“空”的值,它和0
以及空字符串''
不同,0
是一个数值,''
表示长度为0的字符串,而null
表示“空”。
在其他语言中,也有类似JavaScript的null
的表示,例如Java也用null
,Swift用nil
,Python用None
表示。但是,在JavaScript中,还有一个和null
类似的undefined
,它表示“未定义”。
JavaScript的设计者希望用null
表示一个空的值,而undefined
表示值未定义。事实证明,这并没有什么卵用,区分两者的意义不大。大多数情况下,我们都应该用null
。undefined
仅仅在判断函数参数是否传递的情况下有用。
JavaScript对象的键都是字符串类型,值可以是任意数据类型。上述person
对象一共定义了6个键值对,其中每个键又称为对象的属性,例如,person
的name
属性为'Bob'
,zipcode
属性为null
。
要获取一个对象的属性,我们用对象变量.属性名
的方式:
变量
变量的概念基本上和初中代数的方程变量是一致的,只是在计算机程序中,变量不仅可以是数字,还可以是任意数据类型。
变量在JavaScript中就是用一个变量名表示,变量名是大小写英文、数字、$
和_
的组合,且不能用数字开头。变量名也不能是JavaScript的关键字,如if
、while
等。申明一个变量用var
语句,比如:
在JavaScript中,使用等号=
对变量进行赋值。可以把任意数据类型赋值给变量,同一个变量可以反复赋值,而且可以是不同类型的变量,但是要注意只能用var
申明一次,例如:
这种变量本身类型不固定的语言称之为动态语言,与之对应的是静态语言。静态语言在定义变量时必须指定变量类型,如果赋值的时候类型不匹配,就会报错。例如Java是静态语言,赋值语句如下:
如果从数学上理解x = x + 2
那无论如何是不成立的,在程序中,赋值语句先计算右侧的表达式x + 2
,得到结果12
,再赋给变量x
。由于x
之前的值是10
,重新赋值后,x
的值变成12
。
使用console.log()
代替alert()
的好处是可以避免弹出烦人的对话框。
strict模式
JavaScript在设计之初,为了方便初学者学习,并不强制要求用var
申明变量。这个设计错误带来了严重的后果:如果一个变量没有通过var
申明就被使用,那么该变量就自动被申明为全局变量:
在同一个页面的不同的JavaScript文件中,如果都不用var
申明,恰好都使用了变量i
,将造成变量i
互相影响,产生难以调试的错误结果。
使用var
申明的变量则不是全局变量,它的范围被限制在该变量被申明的函数体内(函数的概念将稍后讲解),同名变量在不同的函数体内互不冲突。
为了修补JavaScript这一严重设计缺陷,ECMA在后续规范中推出了strict模式,在strict模式下运行的JavaScript代码,强制通过var
申明变量,未使用var
申明变量就使用的,将导致运行错误。
这是一个字符串,不支持strict模式的浏览器会把它当做一个字符串语句执行,支持strict模式的浏览器将开启strict模式运行JavaScript。
字符串
如果有很多变量需要连接,用+
号就比较麻烦。ES6新增了一种模板字符串,表示方法和上面的多行字符串一样,但是它会自动替换字符串中的变量:
数组
大多数其他编程语言不允许直接改变数组的大小,越界访问索引会报错。然而,JavaScript的Array
却不会有任何错误。在编写代码时,不建议直接修改Array
的大小,访问索引时要确保索引不会越界。
对象
JavaScript的对象是一种无序的集合数据类型,它由若干键值对组成。
JavaScript的对象用于描述现实世界中的某个对象。例如,为了描述“小明”这个淘气的小朋友,我们可以用若干键值对来描述他:
JavaScript用一个{...}
表示一个对象,键值对以xxx: xxx
形式申明,用,
隔开。注意,最后一个键值对不需要在末尾加,
,如果加了,有的浏览器(如低版本的IE)将报错。
上述对象申明了一个name
属性,值是'小明'
,birth
属性,值是1990
,以及其他一些属性。最后,把这个对象赋值给变量xiaoming
后,就可以通过变量xiaoming
来获取小明的属性了:
访问属性是通过.
操作符完成的,但这要求属性名必须是一个有效的变量名。如果属性名包含特殊字符,就必须用''
括起来:
xiaohong
的属性名middle-school
不是一个有效的变量,就需要用''
括起来。访问这个属性也无法使用.
操作符,必须用['xxx']
来访问:
也可以用xiaohong['name']
来访问xiaohong
的name
属性,不过xiaohong.name
的写法更简洁。我们在编写JavaScript代码的时候,属性名尽量使用标准的变量名,这样就可以直接通过object.prop
的形式访问一个属性了。
实际上JavaScript对象的所有属性都是字符串,不过属性对应的值可以是任意数据类型。
如果访问一个不存在的属性会返回什么呢?JavaScript规定,访问不存在的属性不报错,而是返回undefined
:
因为toString
定义在object
对象中,而所有对象最终都会在原型链上指向object
,所以xiaoming
也拥有toString
属性。
要判断一个属性是否是xiaoming
自身拥有的,而不是继承得到的,可以用hasOwnProperty()
方法:
条件判断
JavaScript使用if () { ... } else { ... }
来进行条件判断。例如,根据年龄显示不同内容,可以用if
语句实现如下:
省略{}
的危险之处在于,如果后来想添加一些语句,却忘了写{}
,就改变了if...else...
的语义,例如:
上述代码的else
子句实际上只负责执行console.log('age < 18');
,原有的alert('teenager');
已经不属于if...else...
的控制范围了,它每次都会执行。
相反地,有{}
的语句就不会出错:
但是我们通常把else if
连写在一起,来增加可读性。这里的else
略掉了{}
是没有问题的,因为它只包含一个if
语句。注意最后一个单独的else
不要略掉{}
。
请注意,if...else...
语句的执行特点是二选一,在多个if...else...
语句中,如果某个条件成立,则后续就不再继续判断了。
试解释为什么下面的代码显示的是teenager
:
由于age
的值为20
,它实际上同时满足条件age >= 6
和age >= 18
,这说明条件判断的顺序非常重要。请修复后让其显示adult
。
如果if
的条件判断语句结果不是true
或false
怎么办?例如:
JavaScript把null
、undefined
、0
、NaN
和空字符串''
视为false
,其他值一概视为true
,因此上述代码条件判断的结果是true
。
循环
让我们来分析一下for
循环的控制条件:
- i=1 这是初始条件,将变量i置为1;
- i<=10000 这是判断条件,满足时就继续循环,不满足就退出循环;
- i++ 这是每次循环后的递增条件,由于每次循环后变量i都会加1,因此它终将在若干次循环后不满足判断条件
i<=10000
而退出循环。
for
循环的3个条件都是可以省略的,如果没有退出循环的判断条件,就必须使用break
语句退出循环,否则就是死循环:
由于Array
也是对象,而它的每个元素的索引被视为对象的属性,因此,for ... in
循环可以直接循环出Array
的索引:
while
for
循环在已知循环的初始和结束条件时非常有用。而上述忽略了条件的for
循环容易让人看不清循环的逻辑,此时用while
循环更佳。
while
循环只有一个判断条件,条件满足,就不断循环,条件不满足时则退出循环。比如我们要计算100以内所有奇数之和,可以用while循环实现:
do ... while
最后一种循环是do { ... } while()
循环,它和while
循环的唯一区别在于,不是在每次循环开始的时候判断条件,而是在每次循环完成的时候判断条件:
小结
循环是让计算机做重复任务的有效的方法,有些时候,如果代码写得有问题,会让程序陷入“死循环”,也就是永远循环下去。JavaScript的死循环会让浏览器无法正常显示或执行当前页面的逻辑,有的浏览器会直接挂掉,有的浏览器会在一段时间后提示你强行终止JavaScript的执行,因此,要特别注意死循环的问题。
在编写循环代码时,务必小心编写初始条件和判断条件,尤其是边界值。特别注意i < 100
和i <= 100
是不同的判断逻辑。
函数定义和调用
上述abs()
函数的定义如下:
function
指出这是一个函数定义;abs
是函数的名称;(x)
括号内列出函数的参数,多个参数以,
分隔;{ ... }
之间的代码是函数体,可以包含若干语句,甚至可以没有任何语句。
请注意,函数体内部的语句在执行时,一旦执行到return
时,函数就执行完毕,并将结果返回。因此,函数内部通过条件判断和循环可以实现非常复杂的逻辑。
如果没有return
语句,函数执行完毕后也会返回结果,只是结果为undefined
。
由于JavaScript的函数也是一个对象,上述定义的abs()
函数实际上是一个函数对象,而函数名abs
可以视为指向该函数的变量。
在这种方式下,function (x) { ... }
是一个匿名函数,它没有函数名。但是,这个匿名函数赋值给了变量abs
,所以,通过变量abs
就可以调用该函数。
上述两种定义完全等价,注意第二种方式按照完整语法需要在函数体末尾加一个;
,表示赋值语句结束。
arguments
JavaScript还有一个免费赠送的关键字arguments
,它只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数。arguments
类似Array
但它不是一个Array
:
利用arguments
,你可以获得调用者传入的所有参数。也就是说,即使函数不定义任何参数,还是可以拿到参数的值:
实际上arguments
最常用于判断传入参数的个数。你可能会看到这样的写法:
为了获取除了已定义参数a
、b
之外的参数,我们不得不用arguments
,并且循环要从索引2
开始以便排除前两个参数,这种写法很别扭,只是为了获得额外的rest
参数,有没有更好的方法?
rest参数只能写在最后,前面用...
标识,从运行结果可知,传入的参数先绑定a
、b
,多余的参数以数组形式交给变量rest
,所以,不再需要arguments
我们就获取了全部参数。
如果传入的参数连正常定义的参数都没填满,也不要紧,rest参数会接收一个空数组(注意不是undefined
)。
因为rest参数是ES6新标准,所以你需要测试一下浏览器是否支持。请用rest参数编写一个sum()
函数,接收任意个参数并返回它们的和:
方法
但是,如果我们给xiaoming
绑定一个函数,就可以做更多的事情。比如,写个age()
方法,返回xiaoming
的年龄:
绑定到对象上的函数称为方法,和普通函数也没啥区别,但是它在内部使用了一个this
关键字,这个东东是什么?
在一个方法内部,this
是一个特殊变量,它始终指向当前对象,也就是xiaoming
这个变量。所以,this.birth
可以拿到xiaoming
的birth
属性。
也是不行的!要保证this
指向正确,必须用obj.xxx()
的形式调用!
由于这是一个巨大的设计错误,要想纠正可没那么简单。ECMA决定,在strict模式下让函数的this
指向undefined
,因此,在strict模式下,你会得到一个错误:
单独调用函数getAge()
怎么返回了NaN
?请注意,我们已经进入到了JavaScript的一个大坑里。
JavaScript的函数内部如果调用了this
,那么这个this
到底指向谁?
答案是,视情况而定!
如果以对象的方法形式调用,比如xiaoming.age()
,该函数的this
指向被调用的对象,也就是xiaoming
,这是符合我们预期的。
如果单独调用函数,比如getAge()
,此时,该函数的this
指向全局对象,也就是window
。
结果又报错了!原因是this
指针只在age
方法的函数内指向xiaoming
,在函数内部定义的函数,this
又指向undefined
了!(在非strict模式下,它重新指向全局对象window
!)
修复的办法也不是没有,我们用一个that
变量首先捕获this
:
apply
虽然在一个独立的函数调用中,根据是否是strict模式,this
指向undefined
或window
,不过,我们还是可以控制this
的指向的!
要指定函数的this
指向哪个对象,可以用函数本身的apply
方法,它接收两个参数,第一个参数就是需要绑定的this
变量,第二个参数是Array
,表示函数本身的参数。
装饰器
利用apply()
,我们还可以动态改变函数的行为。
JavaScript的所有对象都是动态的,即使内置的函数,我们也可以重新指向新的函数。
现在假定我们想统计一下代码一共调用了多少次parseInt()
,可以把所有的调用都找出来,然后手动加上count += 1
,不过这样做太傻了。最佳方案是用我们自己的函数替换掉默认的parseInt()
:
标准对象
Date
JavaScript的月份范围用整数表示是0~11,0
表示一月,1
表示二月……,所以要表示6月,我们传入的是5
!这绝对是JavaScript的设计者当时脑抽了一下,但是现在要修复已经不可能了。
时区
Date
对象表示的时间总是按浏览器所在时区显示的,不过我们既可以显示本地时间,也可以显示调整后的UTC时间:
那么在JavaScript中如何进行时区转换呢?实际上,只要我们传递的是一个number
类型的时间戳,我们就不用关心时区转换。任何浏览器都可以把一个时间戳正确转换为本地时间。
时间戳是个什么东西?时间戳是一个自增的整数,它表示从1970年1月1日零时整的GMT时区开始的那一刻,到现在的毫秒数。假设浏览器所在电脑的时间是准确的,那么世界上无论哪个时区的电脑,它们此刻产生的时间戳数字都是一样的,所以,时间戳可以精确地表示一个时刻,并且与时区无关。
所以,我们只需要传递时间戳,或者把时间戳从数据库里读出来,再让JavaScript自动转换为当地时间就可以了。
RegExp
字符串是编程时涉及到的最多的一种数据结构,对字符串进行操作的需求几乎无处不在。比如判断一个字符串是否是合法的Email地址,虽然可以编程提取@
前后的子串,再分别判断是否是单词和域名,但这样做不但麻烦,而且代码难以复用。
正则表达式是一种用来匹配字符串的强有力的武器。它的设计思想是用一种描述性的语言来给字符串定义一个规则,凡是符合规则的字符串,我们就认为它“匹配”了,否则,该字符串就是不合法的。
所以我们判断一个字符串是否是合法的Email的方法是:
-
创建一个匹配Email的正则表达式;
-
用该正则表达式去匹配用户的输入来判断是否合法。
因为正则表达式也是用字符串表示的,所以,我们要首先了解如何用字符来描述字符。
在正则表达式中,如果直接给出字符,就是精确匹配。用\d
可以匹配一个数字,\w
可以匹配一个字母或数字,所以:
-
'00\d'
可以匹配'007'
,但无法匹配'00A'
; -
'\d\d\d'
可以匹配'010'
; -
'\w\w'
可以匹配'js'
;
.
可以匹配任意字符,所以:
'js.'
可以匹配'jsp'
、'jss'
、'js!'
等等。
要匹配变长的字符,在正则表达式中,用*
表示任意个字符(包括0个),用+
表示至少一个字符,用?
表示0个或1个字符,用{n}
表示n个字符,用{n,m}
表示n-m个字符:
来看一个复杂的例子:\d{3}\s+\d{3,8}
。
我们来从左到右解读一下:
-
\d{3}
表示匹配3个数字,例如'010'
; -
\s
可以匹配一个空格(也包括Tab等空白符),所以\s+
表示至少有一个空格,例如匹配' '
,'\t\t'
等; -
\d{3,8}
表示3-8个数字,例如'1234567'
。
综合起来,上面的正则表达式可以匹配以任意个空格隔开的带区号的电话号码。
如果要匹配'010-12345'
这样的号码呢?由于'-'
是特殊字符,在正则表达式中,要用'\'
转义,所以,上面的正则是\d{3}\-\d{3,8}
。
但是,仍然无法匹配'010 - 12345'
,因为带有空格。所以我们需要更复杂的匹配方式。
进阶
要做更精确地匹配,可以用[]
表示范围,比如:
-
[0-9a-zA-Z\_]
可以匹配一个数字、字母或者下划线; -
[0-9a-zA-Z\_]+
可以匹配至少由一个数字、字母或者下划线组成的字符串,比如'a100'
,'0_Z'
,'js2015'
等等; -
[a-zA-Z\_\$][0-9a-zA-Z\_\$]*
可以匹配由字母或下划线、$开头,后接任意个由一个数字、字母或者下划线、$组成的字符串,也就是JavaScript允许的变量名; -
[a-zA-Z\_\$][0-9a-zA-Z\_\$]{0, 19}
更精确地限制了变量的长度是1-20个字符(前面1个字符+后面最多19个字符)。
A|B
可以匹配A或B,所以(J|j)ava(S|s)cript
可以匹配'JavaScript'
、'Javascript'
、'javaScript'
或者'javascript'
。
^
表示行的开头,^\d
表示必须以数字开头。
$
表示行的结束,\d$
表示必须以数字结束。
你可能注意到了,js
也可以匹配'jsp'
,但是加上^js$
就变成了整行匹配,就只能匹配'js'
了。
RegExp
有了准备知识,我们就可以在JavaScript中使用正则表达式了。
JavaScript有两种方式创建一个正则表达式:
第一种方式是直接通过/正则表达式/
写出来,第二种方式是通过new RegExp('正则表达式')
创建一个RegExp对象。
全局匹配可以多次执行exec()
方法来搜索一个匹配的字符串。当我们指定g
标志后,每次运行exec()
,正则表达式本身会更新lastIndex
属性,表示上次匹配到的最后索引:
小结
正则表达式非常强大,要在短短的一节里讲完是不可能的。要讲清楚正则的所有内容,可以写一本厚厚的书了。如果你经常遇到正则表达式的问题,你可能需要一本正则表达式的参考书。
JSON
JSON是JavaScript Object Notation的缩写,它是一种数据交换格式。
在JSON出现之前,大家一直用XML来传递数据。因为XML是一种纯文本格式,所以它适合在网络上交换数据。XML本身不算复杂,但是,加上DTD、XSD、XPath、XSLT等一大堆复杂的规范以后,任何正常的软件开发人员碰到XML都会感觉头大了,最后大家发现,即使你努力钻研几个月,也未必搞得清楚XML的规范。
终于,在2002年的一天,道格拉斯·克罗克福特(Douglas Crockford)同学为了拯救深陷水深火热同时又被某几个巨型软件企业长期愚弄的软件工程师,发明了JSON这种超轻量级的数据交换格式。
道格拉斯同学长期担任雅虎的高级架构师,自然钟情于JavaScript。他设计的JSON实际上是JavaScript的一个子集。在JSON中,一共就这么几种数据类型:
以及上面的任意组合。
并且,JSON还定死了字符集必须是UTF-8,表示多语言就没有问题了。为了统一解析,JSON的字符串规定必须用双引号""
,Object的键也必须用双引号""
。
由于JSON非常简单,很快就风靡Web世界,并且成为ECMA标准。几乎所有编程语言都有解析JSON的库,而在JavaScript中,我们可以直接使用JSON,因为JavaScript内置了JSON的解析。
把任何JavaScript对象变成JSON,就是把这个对象序列化成一个JSON格式的字符串,这样才能够通过网络传递给其他计算机。
如果我们收到一个JSON格式的字符串,只需要把它反序列化成一个JavaScript对象,就可以在JavaScript中直接使用这个对象了。
如果我们还想要精确控制如何序列化小明,可以给xiaoming
定义一个toJSON()
的方法,直接返回JSON应该序列化的数据:
创建对象
JavaScript的所有数据都可以看成对象,那是不是我们已经在使用面向对象编程了呢?
当然不是。如果我们只使用Number
、Array
、string
以及基本的{...}
定义的对象,还无法发挥出面向对象编程的威力。
JavaScript的面向对象编程和大多数其他语言如Java、C#的面向对象编程都不太一样。如果你熟悉Java或C#,很好,你一定明白面向对象的两个基本概念:
-
类:类是对象的类型模板,例如,定义
Student
类来表示学生,类本身是一种类型,Student
表示学生类型,但不表示任何具体的某个学生; -
实例:实例是根据类创建的对象,例如,根据
Student
类可以创建出xiaoming
、xiaohong
、xiaojun
等多个实例,每个实例表示一个具体的学生,他们全都属于Student
类型。
所以,类和实例是大多数面向对象编程语言的基本概念。
不过,在JavaScript中,这个概念需要改一改。JavaScript不区分类和实例的概念,而是通过原型(prototype)来实现面向对象编程。
原型是指当我们想要创建xiaoming
这个具体的学生时,我们并没有一个Student
类型可用。那怎么办?恰好有这么一个现成的对象:
xiaoming
有自己的name
属性,但并没有定义run()
方法。不过,由于小明是从Student
继承而来,只要Student
有run()
方法,xiaoming
也可以调用:
JavaScript的原型链和Java的Class区别就在,它没有“Class”的概念,所有对象都是实例,所谓继承关系不过是把一个对象的原型指向另一个对象而已。
如果你把xiaoming
的原型指向其他对象:
在JavaScrip代码运行时期,你可以把xiaoming
从Student
变成Bird
,或者变成任何对象。
请注意,上述代码仅用于演示目的。在编写JavaScript代码时,不要直接用obj.__proto__
去改变一个对象的原型,并且,低版本的IE也无法使用__proto__
。Object.create()
方法可以传入一个原型对象,并创建一个基于该原型的新对象,但是新对象什么属性都没有,因此,我们可以编写一个函数来创建xiaoming
: