简介
JavaScript遵循ECMASCript核心语法规范,同时引入DOM(Document Object Model)和BOM(Browser Object Model)概念。
引入JS
有至少两种方式可以使用JavaScript,一是在用<script>
和</script>
标签中直接编写JS代码;二是使用<script src="demo.js">
引入单独的demo.js文件。
注释
有两种方式可以为JS代码添加注释,
- // 添加单行注释
- /* */ 添加多行注释
基本数据类型
JS中有七种基本数据类型,
- number,数字,不区分整型和浮点型
- string,字符串,即使用单引号或者双引号包围的文本。多个字符串可以使用’+'拼接成一个字符串。
- boolean,布尔,取值true或false。
- null,表示对象不存在或者被显示赋值为null,使用关键字
null
表示。 - undefined, 表示属性或者方法不存在,或者未赋值,即没有初始值,使用关键字
undefined
表示。 - symbol,ES6新引入,可以使用Symbol(data)和Symbol.for(data)创建symbol。前者每次调用都会生成不同的symbol;后者又称为全局Symbol,如果data值一样,则Symbol也是相等的。
- object,对象,使用
{}
定义。
当使用
typeof
判断类型时,typeof null
返回object
。
变量
有三种定义变量的方式: var
、let
和const
,后两者是ES6新引入的。
- var, ES5使用的关键字,可以定义局部变量,也可以定义全局变量。
- let, 定义块级变量,可连续赋值。
- const, 实际定义的是常量,具有块级作用域。因为const定义的变量一旦被赋值,就不能更改,如果强行更改,会报
TypeError
异常。且const变量必须在定义时就进行赋值。
如果声明变量时缺少var、let或const,则该变量被视为全局变量,要尽量避免这种情况。
++和–运算符
又称自增和自减,顾名思义,就是分别将自身值加1和减1。
字符串
字符串拼接
使用+
号进行拼接。
字符串插值
使用${placeHolder}
进行占位,可以显示出placeHolder的实际值,效果上类似于字符串拼接,但是字符串必须使用` (反引号)包围,而不是引号。
let name = 'Designer';
console.log(`My name is ${name}`);
结果:
My name is Designer
多行字符串
使用``
(反引号)包围多行字符串,省略了不停敲击\n
的麻烦。
类型判断 typeof
打印变量类型。
let name = 'Designer';
console.log(typeof name);
结果:
string
条件语句
主要有if
和switch
两大条件语句。
条件的产生
- 比较运算符:
===
恒等于,表示值是否相等;!==
表示不等于;===
和==
的主要区别在于,如果类型不同,前者直接返回false,而后者则会做一次类型转换,然后再比较,所以前者较严格。比如null
和undefined
在使用===
比较时返回false,但在使用==
比较时返回true。 - 逻辑运算符:
&&
(与)、||
(或)、!
(非)。 - 真值和假值,以下值均为假值,假值在条件语句中被解释成
false
。
- 0
- 空字符串
- null
- undefined
- NaN
if 语句
if … else if … else …
可以使用三元运算符
condition ? express_when_true : express_when_false
改装if ... else ...
语句,但是当有多个else if
时这样改写反而更麻烦。
switch 语句
switch (condition) {
case one:
...
break;
case two:
...
break;
...
default :
break;
}
循环
for
循环
for (let num = 0; num < 4; num++) {
console.log(num)
};
for
循环还有另一种更高级、更简洁的使用方式: for .. in
, 他会遍历所有元素,不用担心越界问题,而且对访问对象属性同样可行。
const arr = [1, 2, 4, 5];
for (let i in arr) {
console.log(arr[i]);
}
while
循环
let num = 0;
while (num < 4) {
console.log(num);
num++;
}
do...while
循环
let num = 0;
do {
console.log(num);
num++;
} while (num < 4);
循环可以嵌套使用; 也可以使用关键字
break
退出当前循环;
函数
使用function 关键字声明函数。
默认参数
ES6引入默认参数的概念,即在函数声明时同时为某些参数指定默认值,当函数调用时,如果这些参数未被赋值,则使用默认值。
比如:
//函数声明
function logName(name='Designer Feng'){
console.log(name);
}
//函数调用
logName();
结果:
Designer Feng
返回值
使用return 关键字指明返回值,但是并不需要在函数头部对返回值类型进行声明,因为javascript本身也是弱类型语言。调用者可以准确的接收到该返回值。如果没有指定返回值,默认返回
undefined
。
函数表达式与匿名函数
匿名函数:顾名思义就是没有名称的函数,但是其他部分如关键字function
、参数列表和函数体等都与一般函数一样。
函数表达式:使用变量指向一个匿名函数,那么该变量本质上就具有了函数的功能。
如函数表达式的例子:
//函数式声明
const printName = function (name) {
console.log('Hello, ' + name);
}
//函数式调用
printName('Designer_Codecademy');
输出:
Hello, Designer_Codecademy
箭头函数
使用胖箭头=>
定义函数,并且省略function
关键字,如下:
const myArrawFunction = (x, y) => {
console.log(x + y);
};
使用时和普通函数的调用一样:
myArrawFunction(1, 4);
输出:
5
箭头函数的其他特点:
- 如果只有一个参数,小括号’()'可以省略;但是如果有0个或者多个参数,不能省略。
- 如果函数体只有一行代码,那么花括号’{}'和
return
语句都可以省略,如,
const add = number => number + number;
高阶函数
形参为函数或者返回一个函数,这样的函数称为高阶函数。
函数是作为参数传入高阶函数的,所以不能带括号,就像传递普通参数一样,否则就变成了多个函数调用。
如下,演示高阶函数和普通函数的差异:
const func1 = () => {
return 1;
};
//高阶函数定义
const highOrderFunc = (funcParam) => {
console.log(funcParam());
};
//普通函数定义
const normalFunc = (value) => {
console.log(value);
};
//高阶函数调用,注意参数不带括号,函数本身就是入参。
highOrderFunc(func1);
//普通函数调用,先执行内部函数,返回结果作为外部函数的入参。
normalFunc(func1());
生成器 generator
JavaScript中的生成器和Python中的生成器很像,看起来像个函数,使用
function*
+yield
定义,yield
能使其运行中断并返回期望值。
定义和使用如下:
function* gen(x) {
yield x + 1;
yield x + 2;
}
const gene = gen(2);
console.log(gene.next());
console.log(gene.next());
console.log(gene.next());
输出:
{ value: 3, done: false }
{ value: 4, done: false }
{ value: undefined, done: true }
生成器只有当调用next()方法时才会执行,当遇到yield
时返回,返回值是一个对象,当无值返回时也不会报错,只不过value变成undefined
且done变为true
。所以如果只想要有效的value值的话,要做一些判断。或者使用for...of
遍历生成器,他只会返回有效的value值,如下:
for (var i of gen(2)) {
console.log(i);
}
输出:
3
4
函数总结
- 函数也是对象,所以也有对象该有的基本属性和方法,如toString();
- 可以将函数名赋给另一个变量,新变量的
name
属性值就是原函数的名称,且新变量可以直接作为函数调用。
作用域
和其他任何编程语言一样,变量都是有作用域的,分为全局作用域、局部作用域和块级作用域(ES6引入块级作用域)。
全局作用域
顾名思义,在程序的任何位置都能访问到该变量,定义在函数体外部的变量具有全局作用域,使用var
声明。在web中,这样的变量实际上被绑定到window对象上了。为了减少全局变量名字冲突的问题,可以使用命名空间,即定义一个自己的对象,然后把变量绑定到该对象。如:
var Person = {};
Person.name = 'Feng';
Person.age = 18;
局部作用域
如果定义在函数体内的变量使用var
声明,则该变量具有局部作用域,即无论何时声明的该变量,在函数体内的任何位置都是可以使用的(但是如果在未赋值前使用的话,值是undefined,但是不会报错。)。
块级作用域
使用{}
包起来的范围叫做块,在该范围内如果使用let
和const
来声明变量,则该变量具有块级作用域,只能在该块中使用,且必须先声明后使用。
局部作用域与块级作用域的区别
前面提到过,当在函数中使用var
声明变量时,变量在该函数内部任何位置都是可以访问的,无论在哪里声明,因为实际上该变量的声明被提升到了函数体首行,叫做变量提升,如下:
const arr = [1, 2, 3];
function print() {
for (var i in arr) {
console.log(arr[i]);
}
console.log(arr[i]);
}
print();
输出:
1
2
3
3
可以看出,尽管i
是在for循环中声明的,但是for循环外仍然可以使用,因为i
具有局部作用域,实际声明被提升到了函数体的首行,所以在哪个位置都能使用。
但是当我们把上面代码中的var
改为let
后,就会报ReferenceError: i is not defined
,这个错误是for循环外的console.log(arr[i])引起的。之所以会报错,是因为let
定义的变量具有块级作用域,不会进行变量提升,所以只在{}
范围内有效(此时的块指for
循环体)。所以如果把let
的声明放在函数{}中声明,也是没有问题的(此时的块变成了函数体),如下:
const arr = [1, 2, 3];
function print() {
let i;
for (i in arr) {
console.log(arr[i]);
}
console.log(arr[i]);
}
print();
数组
最简单的定义数组的方式是使用方括号[]
。
数组元素可以是不同的类型。
数组下标从0开始。
字符串可以看成是多个字符组成的数组,因此可以使用下标访问特定位置的字符,如:
const name = 'Designer Feng';
console.log(name[4]);
输出:
g
使用length
属性获取数组长度,也可以为length
重新赋值,这样会引起数组长度的变化。如果新的length值大于之前的值,则数组其它位置则自动填充空元素;如果新的length值小于之前的值,则会将数组中大于length的元素直接删除。所以最好不要直接修改length值,访问数组时也注意不要越界(虽然越界也不会报错)。
常用方法
- push(item), 向数组中添加元素。
- pop(),移除数组中最后一个元素,并且返回该元素。
- shift(),删除数组头部的元素,并返回该元素。
- unshift(item),添加元素到数组头部。
- indexOf(item),返回item的索引位置。
- slipce(start,end),切割数组(不含end),返回切割后的数组,不会修改原数组。
- splice(start, count,newItems),从start开始,删除count个元素,如果指定newItem,那么newItems会填充到删除的位置,达到替换的效果。如果start<0,则会从0开始;如果start>=.length,不会删除任何元素;如果count<=0,不会删除任何元素;如果count>=.length,也不会报错,但是会删除start后的所有元素。
如end>.length时的删除操作:
const name = ['Jack', 'Luis', 'Blues'];
name.splice(1, 6);
输出:
[ 'Jack' ]
使用’Feng’和’Designer’替换’John’和’Blues’:
const names = ['John', 'Blues', 'Jack', 'Jorge'];
names.splice(0, 2, 'Feng', 'Designer');
console.log(names);
输出:
[ 'Feng', 'Designer', 'Jack', 'Jorge' ]
内嵌数组
即数组元素本身又是数组,其实就是多维数组,如下是一个二维数组,
const arr = ['Luis', 'Neo', ['Jhon', 'David']];
访问元素’David’:
const david = arr[2][1];
数组元素迭代
forEach()
arr.forEach(func)
指定一个回调函数func,arr中的每个元素依次传入函数func执行;func有三个参数,分别是元素值、元素索引和数组本身,但是一般只保留第一个参数,因为对于数组遍历,第一个参数已经足够了。forEach返回undefined
。
const arr = [1, 2, 'ab'];
arr.forEach((element, index, myArray) => {
console.log(`index = ${index}, element = ${element}`);
});
输出:
index = 0, element = 1
index = 1, element = 2
index = 2, element = ab
map()
arr.map(func) //与forEach
类似,也是接受一个函数作为入参,将数组arr中的元素依次传入func,返回计算之后的新值。不同的是,map
会返回一个新的数组,新的数组元素就是func计算后的值,所以func必须有返回值,如果没有指定返回值,则默认都是undefined
,如下:
const arr = [1, 2, 3, 4];
- 指定返回值
const newArr = arr.map(element => {
return element * element;
})
console.log(newArr);
输出:
[ 1, 4, 9, 16 ]
- 不指定返回值
const newArr = arr.map(element => {
// return element * element;
element*element;
})
输出:
[ undefined, undefined, undefined, undefined ]
filter()
arr.filter(func) //过滤器,将原数组arr通过func过滤后新生新数组返回。依次将arr中元素传给func,如果func返回true,则保留该元素,否则过滤掉,最终所有保留下来的元素组成新的数组。
findIndex()
arr.findIndex(func) //返回首次匹配的元素索引,匹配规则由func指定,返回true则匹配成功。
reduce()
arr.reduce(func[,initialValue])
- func接收两个参数作为输入;initialValue可选。
- 首次迭代时,如果intialValue未指定,则从arr中取出两个元素传给func;如果指定了intialValue,则只取一个元素,另一个使用initialValue;
- func返回一个值,作为下次迭代的第一个输入参数,然后再从arr中取一个元素,将这两个参数再次传给func。
- 依次类推,最后reduce返回计算后的总结果。
例子:
const arr = [1, 2, 3, 4];
未指定intialValue:
const result = arr.reduce((first, second) => {
console.log(`first: ${first}, second: ${second}`);
return first + second;
});
console.log('result:' + result);
输出:
first: 1, second: 2
first: 3, second: 3
first: 6, second: 4
result:10
指定了intialValue:10
const arr = [1, 2, 3, 4];
const result = arr.reduce((first, second) => {
console.log(`first: ${first}, second: ${second}`);
return first + second;
}, 10);
console.log('result:' + result);
输出:
first: 10, second: 1
first: 11, second: 2
first: 13, second: 3
first: 16, second: 4
result:20
map
、forEach
、filter
、reduce
等高阶函数的回调函数实际上都能接收多个参数,但是一般我们只用关注必须的一个或两个参数,其它可选的参数不用管。
对象
javascript中对象使用{}
定义,对象中的数据都是key:value形式,称为属性,key是字符串;value可以是任意类型。就像是JSON对象(JSON:JavaScript Object Notation,JavaScript对象表示法),只不过JSON表示的类型范围要小,只支持number
、boolean
、string
、null
、array []
和object {}
,而且对key和value要求更严格。
可以使用JSON.stringify(obj)将一个JavaScript对象obj转换为JSON对象;或者使用JSON.parse(jsonObj)将一个JSON对象转换为JavaScript对象。
所有JavaScript对象均继承至object。
访问属性
有至少两种方式可以访问对象的属性:
- 使用
.key
,key只能是属性名称本身,就算是值与属性名称相同的变量也不行。 - 使用
[key]
。
第一种方式更加简单,第二种方式更加严谨。因为如之前所述,key是字符串,就这表示key中可能包含空白符等特殊字符,如果此时使用第一种方式会出现字符串的截断,出现语法错误,但是第二种方式不会有此问题,如下,
const me = {
'My Name': 'Designer Feng',
'My Age': 18,
};
console.log(me["My Name"]);
可以使用
for..in
遍历对象中的每个属性。
更新属性
可以直接使用=
为属性赋值,这会出现两种情况:
- 指定的属性已经存在,此时只会更新value值。
- 指定的属性不存在,添加新的key:value对。
可以使用
delete
操作符删除属性,如delete me['My Name']
。
方法
方法是一种特殊的属性,因为其属性值是函数。
使用key:func,来定义方法,其中func是一个标准的javascript 函数。ES6之后可以使用简写的形式(省略冒号和function
关键字)来定义方法,如下:
const designer = {
printAge(age) {
console.log('Age is ' + age);
}
};
designer.printAge(3);
Getter和Setter
getter和setter形式上像两种方法,getter用来获取属性值,setter用来给属性重新赋值。在其他高级编程语言中(如java),属性有明确的访问权限,比如私有属性只能在对象内部使用,如果想在外部也能访问,必须设置公开权限和getter和setter类型方法。
但是javascript的属性本质上是没有访问权限之说的,但是为了达到像其他语言一样的效果,所以约定在属性前加上_
(下划线)表示私有属性,便于给其他开发者说明,该属性是一个私有属性(其实在对象外部仍能直接访问),并且也提供getter和setter方法。
getter
定义getter:
const obj = {
_name: 'Feng',
get getName() {
return `My name is ${this._name}`;
}
};
使用get关键字定义getter,getter方法不能携带任何参数,否则就会报
SyntaxError
异常;要在被访问的属性前加上this
关键字,否则找不到该属性。
使用getter:
console.log(obj.getName);
getter的调用必须去掉(),就像是使用属性一样,因为javascript就是把getter和setter当作属性(虽然他们看起来确实是方法),这样就达到了隐藏原始属性的目的。
setter
定义setter
const obj = {
_name: 'Feng',
get getName() {
return `My name is ${this._name}`;
},
set setName(newName) {
this._name = newName;
}
};
使用
set
关键字定义setter,只能指定一个参数。
使用setter
obj.setName = 'Designer Feng';
像给属性赋值一样直接调用,实际是将新值赋给了setter指定的属性。
类(class)和实例
在任何面向对象(OOP)的编程语言中,都有类
的概念,它用来定义一套模板,然后利用该模板可以生成多个相似的实例(在其它编程语言中实例也叫对象或者实例对象,但是在这里我们还是稍微区分一下,因为我们之前说过JavaScript的对象是通过{}
来定义的),先有类,后有实例。一个模板,多个实例。JavaScript使用class
定义类,使用new
关键字生成实例。
面向对象语言的三大特性:封装、继承和多态。JavaScript有原型继承和class继承(ES6),这里主要关注了后者。
定义类
class Designer {
//构造方法
constructor(name) {
this._name = name;
}
//getter
get getName() {
return this._name;
}
//普通方法
updateName(newName) {
this._name = newName;
}
}
类的定义有以下几个特点:
- 使用
class
关键字定义类的名称。 - 使用
constructor
定义类的构造方法,每次使用new
生成对象时,该方法就会被自动调用。 - 像对象一样,可以定义setter和getter以及普通方法,并且每个方法之间不用逗号隔开。
继承 Inheritance
现实世界中许多事物都是有相似性的,但又不能完全把它们归为一类,比如猫和狗都是动物,有时候当你定义模板时,你需要保留猫和狗的相似性(如哺乳类,宠物等),但又要区分他们的不同特点时(如叫声、夜行性等),那么就会用到继承。
使用extends
关键字指明父类(Animal)和子类(Dog或Cat)之间的关系:
//定义父类Animal
class Animal {
constructor(name) {
this._name = name;
}
}
//定义子类Cat,继承至父类Animal
class Cat extends Animal {
constructor(name, nocturnal) {
super(name);//调用super
this._nocturnal = nocturnal;//新的属性,是否夜行性动物
}
//新的getter
get nocturnal() {
return this._nocturnal;
}
}
//使用
const cat = new Cat('Bob', true);
console.log(cat._name);
console.log(cat._nocturnal);
继承有如下特点:
- 必须在子类constructor的第一行调用super(其实就是调用父类的constructor,所以要注意传递的参数和父类constructor保持一致)。
- 子类继承父类中的属性和方法。
- 子类可以定义自己的属性和方法。
静态方法
在类中使用static
关键字定义的方法。这样的方法只能使用类名.方法名
进行调用,子类和实例都不能调用静态方法。
模块 Module
可以把一些常用的功能或特性整合到单独的文件中,这样就可以在其它地方直接引入该文件,而不必重复编写代码,即可复用(且可导出导入)的代码片段被称为模块。
导出模块
常规的定义一个模块方法:
- 定义一个有意义的对象,即该对象有属性或方法能够供其它模块使用。
- 使用
module.exports
或者export
(ES6)导出该对象。
如下,module_test.js
定义对象:
let Bird = {
color: 'Yellow',
fly: function () {
console.log('Bird is flying.');
}
};
let Cat = {
color: 'White',
};
导出模块:
//导出单个模块
module.exports = Bird;
//导出多个模块
module.exports = { Bird, Cat };
一个文件中是可以定义多个对象的,如果想把多个对象都当作模块导出,可以使用
{}
或[]
将多个对象包起来,实际上就是将这些对象作为元素重新组合成新的对象或者数组。
而新式的export
语法允许在声明对象时就导出(Named Export),如下导出fly:
export function fly(){
...
};
导入模块
可以在其它文件中使用require(file)
或者import
(ES6)将模块引入到当前文件中,这样在模块中定义的对象属性和方法就能被直接使用。
当module_test.js只导出一个模块时,该如何导入和使用?
导出:
module.exports = Bird;
导入:
const bird = require('./module_test.js');
bird.fly();
当module_test.js导出多个模块时,该如何导入和使用?
导出:
module.exports = { Bird, Cat };
导入:
const birdAndCat = require('./module_test.js');
birdAndCat.Bird.fly();
如果导出时使用的是数组,那么导入时可以使用下标进行访问数组中的每个元素。还可以使用
export as
和import as
为对象起别名。
Node环境不识别export
和import
如果JavaScript运行在NodeJS环境时,当使用export
进行导出和import
进行导入时,会报SyntaxError
。比如我使用的VSCode + Code Runner插件运行JS时就会有这样的问题,因为Code Runner运行在node环境中的。根本原因是NodeJS对ES6语法的支持并不十分完善,可以考虑更新NodeJS或者先不理会,使用可以识别的语法:
条目 | Node | 浏览器 |
---|---|---|
模块规范 | CommonJS | ES6 |
导出 | modules.exports;exports | export; export default |
引入 | require | import; require |
浏览器兼容性
ES6相对于ES5的ECMAScript版本,增加了许多特性,如let
、const
、字符串插值等,使得代码编写更加简洁和高效,但并不是所有浏览器都能很好的兼容这些特性,可以访问caniuse 网站查看主流浏览器对新特性的兼容性。
为了解决这样的问题,可以使用预处理工具将ES6语法转换为ES5,如Babel等。将一种语言转化为另一种语言的操作有一个专有术语,叫做TRANSPILATION。
创建Babel环境(在工程目录下执行):
npm init
, 创建package.json文件。npm install babel-cli -D
, 下载Babel命令行工具。npm install babel-preset-env -D
, 下载ES6到ES5的映射关系库。touch .babelrc
, 新建配置文件,并添加以下对象:
{
"presets": ["env"]
}
- 在package.json中添加build编译指令:
src
:要转换的ES6代码目录,-d lib
:转换后的ES5代码目录
...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "babel src -d lib"
}
npm run build
, 编译ES6代码到ES5,其实就是执行第5步中配置好的build
脚本,然后将生成的ES5代码放到lib目录中。
HTTP请求
常见有四种HTTP请求:POST/GET/PUT/DELETE,其实就是对应数据库操作的增/查/改/删。把请求发送给服务器后,服务器按照请求类型操作数据库,然后返回数据给浏览器;
AJAX: Asynchronous JavaScript and XML。
GET
示例:
const xhr = new XMLHttpRequest();
//指定响应数据的类型
xhr.responseType = 'json';
//指定成功后的回调函数
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
//处理响应数据
handleResponse(xhr.response);
}
}
//发送请求
xhr.open('GET', url);
xhr.send();
POST
POST和GET操作基本一样,但是由于POST是把客户端数据传给服务器,所以会携带额外的数据。
xhr.open('POST', url);
xhr.send(data);