“三更梦醒,瞧檐上落下的月,满地银光。”
Web:JavaScript 基础
前言
最后轮到 JS 了,这篇文章并不能帮你熟练使用 JS,但是能帮你理解个大概
这篇都是一些基础啊语法啥的,之后再整一篇 JS 结合 HTML 的文章吧
这里有个好玩的闯关式学习教程:free code camp
一些基础
在前端三剑客 HTML,CSS,JavaScript 中,JS 是唯一的编程语言,因此它更为复杂,但各种功能的背后都离不开它。
最初,JavaScript 在 1995 年由 Netscape 公司的 Brendan Eich 设计实现,由于那时该公司正和 Sun 公司合作,希望它外观看起来像 Java,因此取名为 JavaScript。
遗憾的是如今我们知道两者相差甚远,Java 是典型的强类型语言,而 JS 则是弱类型语言,定义变量的时候不需要规定其数据类型;毕竟 Java 是静态语言,需要先编译成 class 文件再执行,而 JS 则是动态语言,数据类型可以在运行时确定。
当然静态语言的优势在于其严格的代码规范,因此 IDE 有很强的代码感知能力,一些小错误会即时报错,所以很适合大型或复杂的系统;反之动态语言的代码量更少更简洁,能让开发者更注重业务逻辑。
JavaScript 的文件后缀为.js,所以它也简称为 JS。
而著名的ES6(ECMAScript 6)则是 2015 年 6 月发布的 JS 语言标准,正式名为ECMAScript 2015(ES2015)。在 ES6 中,引入了很多新的语法功能、方法和特性,如今已得到广泛的应用。
和 css 类似,可以在<head>中添加一个<script>元素来绑定一个 HTML 和 JS 文件:
<script type="text/javascript" src="./test.js"></script>
当然,我们也可以在<body>添加:
<script type="module" src="index.js"></script>
数据类型
和大部分语言类似,JS 的数据类型大体可分为两种:基本类型(Value Type / Primitive Types)和引用类型(Reference Types)
区别也基本没差,基本类型通过值传值,而引用类型通过引用(地址)传值
具体类型如下:
| 数据类型 | 意义 | 类型 |
|---|---|---|
| Number | 数值 | 基本类型 |
| BigInt | 大整数 | 基本类型 |
| String | 字符串 | 基本类型 |
| Boolean | 布尔类型 | 基本类型 |
| Symbol | 唯一标识对象的属性 | 基本类型 |
| undefined | 未赋值变量的默认值 | 基本类型 |
| null | 空对象引用 | 基本类型 |
| Object | 对象 | 引用类型 |
| Function | 函数 | 引用类型 |
| Array | 数组 | 引用类型 |
然后下面对一些数据的细节进行说明
由于 JS 没有区分整数和浮点数,所以不管是整数还是小数,最终都是Number类型
而对于那些不存在的数字(比如0/0),其值为NaN
至于Symbol,它是 ES6 引入的一个新的基本类型,用于解决对象属性名冲突的问题。在 ES6 之前,所有属性名都是字符串,这就容易造成属性名的冲突,特别是想给别人提供的对象添加新的属性或方法的时候(毕竟你不知道它本来有哪些属性或方法)
可以通过Symbol()创建,也可以传入字符串参数作为对该变量的说明
let sym = Symbol(); //创建一个Symbol
let symWithPara = Symbol("name"); //带说明的Symbol
//作为对象的唯一属性
let obj = {
[symWithPara]: "BlackDn",
};
console.log(obj[symWithPara]); //输出'Blackdn'
如果不加参数,比如sym,在控制台输出的就是Symbol();如果加了参数,比如symWithPara,输出的就是Symbol('name')
此外,Symbol()参数只是表示对当前 Symbol 值的描述,因此即便参数相同的两个Symbol其值也是不相等的。
不过要注意作为属性名的时候需要用方括号包裹Symbol,否则就只是symWithPara的字符串而非这个Symbol变量。同样,obj['symWithPara']和obj.symWithPara得到的都不是这个Symbol。
引号的使用
JS 支持三种引号,即单引号'',双引号"",反引号
通常情况下,都是在用字符串的情况下使用引号,单引号''和双引号""都可以表示字符串
const s1 = "hello";
const s2 = "hello";
console.log(s1 === s2); //输出:true
不过如果想字符串中包含引号,就需要用\转义。
所以很多时候为了简洁起见,可以混合使用单双引号,这样就不用转义了
const s1 = 'hello "world"';
const s2 = "hello 'world'";
const s3 = 'hello "world"';
const s4 = "hello 'world'";
console.log(s1 === s3); //输出:true
console.log(s2 === s4); //输出:true
至于反引号 是比较新的,在 ES6 引入
很多时候我们需要用变量来构造字符串,这样就要用+来进行字符串的运算,非常麻烦:
const user = "blackdn";
const s = "hello " + user + " to this world.";
console.log(s); //输出:hello blackdn to this world.
于是引入了反引号 ,支持在字符串中直接解析变量:
const user = "blackdn";
const str = `hello ${user} to this world.`;
console.log(str); //输出:hello blackdn to this world.
当然,反引号的字符串里要用反引号也需要转义。
const str = `hello \`world\`.`;
console.log(str); //输出:hello `world`.
声明变量
三种方式声明变量:let,var和const
let name = "black";
var handsome = "yes";
const age = 18;
之前,JS 只能通过var声明变量,不过 ES6 引入了let。相对于var,let所声明的变量作用域更小,只在其所在的代码块内有效(block-scoped);
而var的作用域则更大,在整个函数域内都存在(function-scoped)
所以原则上是能用let就用let。避免一些意料之外的误写、冲突等错误
for (let i = 0; i < 5; i++) {
console.log(i); //输出1-4
}
console.log(i); //报错ReferenceError: i is not defined
for (var i = 0; i < 5; i++) {}
console.log(i); //输出5
此外,var存在变量提升(Hoisting)现象,即变量可以在声明之前使用,值为undefined。
不过显然,在声明前使用变量是不合逻辑的,因此let纠正了这种现象,在声明前使用let的对象会报错:
console.log(foo); // 输出undefined
var foo = 2;
console.log(bar); // 报错ReferenceError
let bar = 2;
const是声明一个常量不多做解释了,要注意的是,当这个常量是一个引用类型时,比如一个对象或数组,我们仍可对其内容进行修改。
因为我们修改后改变的是值而非地址,而引用类型的保存的是地址,其并没有发生变化,因此这对 JS 来说是合法的:
const person = {
name: "Blackdn",
};
person.name = "White";
console.log(person.name); //输出'White'
逻辑运算
什么if-else的判断语句就不展开讲了,简单来说if,if-else,if-else if等语句都可以用的
还有switch语句也和java没差,都是switch-case-break的形式,就略过啦
这里就简单说一下逻辑运算嗷。
或运算:||
或运算还是老样子,只要有一个为true,则全部为true,就不举例了。
不过在 JS 中,或运算还有特殊的作用,就是用来寻找第一个真值:
result = value1 || value2 || value3;
在上面的式子中,会从左到右进行操作计算,处理每一个操作数时,都将其转化为布尔值。如果结果是 true,就停止计算,返回这个操作数的初始值
如果所有的操作数都被计算过(也就是,转换结果都是 false),则返回最后一个操作数。
不论如何,返回的值是操作数的初始形式,不会做布尔转换。
利用这个特性,我们可以用来做一些意料之外的事情,比如获取变量列表或者表达式中的第一个真值:
let firstName = "";
let lastName = "";
let nickName = "BlackDn";
console.log(firstName || lastName || nickName || "allEmpty"); //输出:BlackDn
另一个用处就是进行短路求值(Short-circuit evaluation)
当进行到达到第一个真值后,会立即返回该值,而无需处理其他参数。
这个特性常用来执行左侧的条件为假时才执行命令。(虽然但是用if他不香吗)
true || console.log("1"); //没有输出
false || alert("alert me when all of others fail"); //输出:"alert me when all of others fail"
与运算:&&
与运算也不用多讲,全为true才返回true
和或运算类似,与运算用来寻找第一个假值
result = value1 && value2 && value3;
同样从左到右进行计算,先转为布尔值,遇到false就停止,返回操作数的初始值。要是全为true就返回最后一个操作数的初始值
注意,与运算 && 的优先级比或运算 || 要高
a && b || c && d 等同于(a && b) || (c && d)(写的时候都给我加上括号,老师教的代码可读性可别忘了!)
非运算:!
……不会指望我讲这个吧? 好吧还是简单说一下,非运算用来取反,没了。
值得一提的是,我们可以用两个非运算!!来将某个值转化为布尔类型:
console.log(!!"non-empty string"); // true
console.log(!!null); // false
原理就是第一个非运算将该值转化为布尔类型并取反,第二个非运算再次取反,就得到了其布尔类型,而非初始数据类型。
当然可以用Boolean函数创建一个布尔对象,从而得到同样的效果:
console.log(Boolean("non-empty string")); // true
console.log(Boolean(null)); // false
== 和 ===
简单来说,==是普通比较,===是严格比较。
==在比较的时候,如果双方的数据类型不相等,则会进行一个转换,比如1 == true(数字和Boolean比较),双方类型不同,会将true转换成1,因此等式成立。同理,"1" == true,0 == false都是成立的。
===就比较直白了,先比较数据类型,如果不同就不成立;如果类型相同了,再比较值,值不同也不成立。只有当类型和值都相等的情况下才会成立。
1 == 1; //true
"1" == 1; //true
0 == false; //true
0 == null; //false
0 == undefined; //false
null == undefined; //true
NaN == NaN; //false NaN和任何数都不相等
3 === 3; //true
3 === "3"; //false
null === undefined; //false
同理,!=和!==也是同样的效果,前者会先进行类型转换,后者的比较内容包括类型。
空值合并运算:??
空值合并运算符(nullish coalescing operator)是 ES6 引入的,当左侧参数为null或undefined的时候,则返回右侧参数:
let result = value1 ?? value2;
如果value1为为null或undefined则result的值为value2。用三元操作符?:表示的话就是:
result = value1 !== null && value1 !== undefined ? value1 : value2;
该运算通常用来提供默认值。
和或运算类似, ?? 可以从一系列的值中选择出第一个 非null/undefined 的值。(因为或运算会将null/undefined视为false)
let firstName = "";
let lastName = "";
let nickName = "BlackDn";
console.log(firstName ?? lastName ?? nickName ?? "allEmpty"); //输出:BlackDn
循环语句
循环其实也没差啦,while循环、for循环、do-while都和 java 一样所以就不说啦
要说唯一的差别那就是 for 循环中定义变量的时候要用let而不是int吧=。=
这里说一下比较特别的for-in和for-of循环,他们和 java 中的for-each比较像
不过for-in循环的内容是下标(数组)或属性(对象)而另一个for-of循环则直接输出元素
因此对于数组推荐使用for-of,对于对象则推荐使用for-of(对象不能使用for-of)
对象只能用for-in
const person = {
name: "Blackdn",
age: 22,
};
for (let prop in person) {
console.log(prop + " " + person[prop]);
}
//输出:
//name Blackdn
//age 22
数组则两个都可以用:
const arr = ["a", "b"];
for (let index in arr) {
console.log(index + " " + arr[index]);
}
//输出:
//0 a
//1 b
for (let val of arr) {
console.log(val);
}
//输出:
//a
//b
常用函数
| 函数 | 作用 |
|---|---|
| Math.random() | 生成随机数([0,1)) |
| Math.floor() | 去掉小数部分,保留整数 |
| parseInt() | 将字符串转为整数类型(parseInt("11", 2)返回3,第二个可选参数表示进制) |
数组 Array
数组大体上没什么好说的,同样是通过下标(index)索引,下标同样是从 0 开始
不过,既然是动态编译的弱类型语言,JS 允许数组的各个元素是不同的数据类型
可以用push方法直接在数组后面添加元素
const array = [1, 2, 3];
array.push(4, 5); //变成[1, 2, 3, 4, 5]
此外,Array 的filter可以对数组元素进行筛选,从而得到新的数组
const numbers = [1, 2, 3, 4, 5];
const filtered = numbers.filter((n) => n % 2 === 0); //结果为[2, 4]
而map能对所有元素进行操作
const numbers = [1, 2, 3, 4, 5];
const mapped = numbers.map((n) => (n += 1)); //结果为[2, 3, 4, 5, 6]
还有reduce,通常进行求和等操作。因为它最终是吧一串元素变成一个值,所以称之为reduce(减少)
const numbers = [1, 2, 3, 4, 5];
const reduced = numbers.reduce((accumulator, current) => accumulator + current);
//reduced结果为:15
然后下面是一些常用函数,更多可以看菜鸟:JavaScript Array 对象
| 函数 | 作用 |
|---|---|
slice() |
选取数组的一部分,并返回一个新数组,slice(4,7)返回原数组下标4-6元素组成的新数组,包含头位置,不包含尾位置 |
join() |
连接数组元素为字符串,传入参数为连接符,默认为逗号,,['hello', 'world'].join('->')结果为'hello->world' |
find() |
返回第一个符合要求的元素,都不符合时返回undefind。需要传入函数作为参数,以此判断是否符合要求。 |
unshift() |
在数组头部增加元素 |
push() |
在数组尾部增加元素 |
pop() |
删除并返回数组的最后一个元素 |
shift() |
删除并返回数组的第一个元素 |
splice() |
从数组中添加或删除元素,会修改原数组。splice(index, quantity, item1...n),参数分别表示删除/添加位置,删除多少元素,添加的新元素 |
includes() |
判断一个数组是否包含一个指定的值。 |
indexOf() |
搜索数组中的元素,并返回它所在的位置。没找到则返回-1 |
forEach() |
对每个元素执行某函数(参数为函数) |
reverse() |
返回逆序的数组 |
字符串 String
字符串也没啥好说的,这里列出一些我遇到过的函数
JS 就是这点不好,你不知道他都有啥函数,除非看到别人用或者自己去查手册。不像 Java 那样能到源码里看=。=
也可以看看菜鸟,里面的方法还挺全的:JavaScript String 对象
| 函数 | 作用 |
|---|---|
slice() |
提取字符串的片断,'coconuts'.slice(4,7)结果为'nut',包含头位置,不包含尾位置 |
indexOf() |
返回子串开头的出现位置:'coconuts'.indexOf('ut')结果为5 |
trim() |
去除字符串两边的空白 |
split() |
把字符串分割为字符串数组,参数为分隔符(为空时不分割,为空字符串''时分割每个字符) |
toLowerCase() |
全变为大写 |
toUpperCase() |
全变为小写 |
concat() |
连接多个字符串 |
startsWith() |
判断字符串是否以某子串开头 |
endsWith() |
判断字符串是否以某子串结尾 |
对象 Object 和 类 Class
定义对象
在 JS 中创建对象比较简单,只需要用括号包括,再用键值对属性名: 值的方式创建属性即可
下面两种声明和获取属性的方式是一样的:
let person = {
name: "Blackdn",
};
console.log(person.name); //输出'Blackdn'
//等同于
let person = {
["name"]: "Blackdn",
};
console.log(person["name"]); //输出'Blackdn'
还要注意一点,如果属性名由变量存储,那么只能通过方括号的方式来获取变量内的属性名
且方括号中的变量名不能加上引号:
let propertyName = "name";
console.log(person.propertyName); //输出'undefined'
console.log(person[propertyName]); //输出'Blackdn'
console.log(person["propertyName"]); //输出'undefined'
定义类
和传统一样,JS 用关键字class标识一个类,不过不同的是我们不需要为其声明属性
类中的属性直接在构造函数中标识,用constructor标识构造函数
当然类也可以有自己的函数,不过定义的时候不需要function关键字
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
introduce() {
return `My name is ${this.name}. I am ${this.age} years old.`;
}
}
继承类
继承同样用的是extend关键字:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
introduce() {
return `My name is ${this.name}. I am ${this.age} years old.`;
}
}
class Student extends Person {
constructor(name, age, klass) {
super(name, age);
this.klass = klass;
}
introduce() {
return `${super.introduce()} I am a Student. I am at Class ${this.klass}.`;
}
}
继承的时候,子类中用super指代父类,可以用super()调用父类的构造方法,也可以用来调用父类的函数
比如Student的introduce方法实际上等同于
My name is ${this.name}. I am ${this.age} years old. I am a Student. I am at Class ${this.klass}.
常用函数
用到的时候再加吧=。=
| 函数 | 作用 |
|---|---|
hasOwnProperty() |
判断某对象是否含有某属性。person.hasOwnProperty('name')等同于'name' in person,都返回true |
Object.keys() |
将对象的所有属性名返回为一个数组。Object.keys(person)返回['name'] |
模块化 Model
就像之前的 Sass 能够用@import导入文件实现模块化,JS 肯定不甘落后
很多时候我们将一个类进行模块化,然后在子类中导入并继承。我们把上面的Person类放在person.js文件中
不同于 Sass,我们不能直接在其他文件导入person.js,而是在导入前需要进行导出
我们可以在定义类的时候用export声明导出:
// person.js
export class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
introduce() {
return `My name is ${this.name}. I am ${this.age} years old.`;
}
}
当然也可以在代码最后声明导出:
我比较喜欢这种方式啦,方便查找修改。
// person.js
class Person {
//···
}
export Person;
既然有了person.js,那么我们再搞一个student.js用来写子类Student
我们的父类Person已经道出了,那么在student.js中就要将其导入:
// student.js
import { Person } from "./person";
class Student extends Person {
constructor(name, age, klass) {
super(name, age);
this.klass = klass;
}
introduce() {
return `${super.introduce()} I am a Student. I am at Class ${this.klass}.`;
}
}
可以看到导入的格式为import {name} from "file";
和 Sass 一样, 文件名不需要加.js后缀
上面的方法称为命名导出(Named Export),还有一种默认导出(Default Export)
就是多加个default关键字:
// person.js
class Person {
//···
}
export default Person;
使用默认导出后,在导入的时候就不需要加中括号了:
// student.js
import Person from "./person";
class Student extends Person {
//···
}
当然也可以结合使用:
// person.js
class Person {
//···
}
export function funToExport() {
console.log("hello");
}
export default Person;
这样student.js中的导入语句就可以结合两者,Person是默认导入的所以不用中括号,函数funToExport()是命名导入所以就要有中括号
// student.js
import Person, { funToExport } from "./person";
class Student extends Person {
//···
}