Web:JavaScript基础

数据类型、变量、语法等

Posted by BlackDn on August 20, 2022

“三更梦醒,瞧檐上落下的月,满地银光。”

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`.

声明变量

三种方式声明变量:letvarconst

let name = "black";
var handsome = "yes";
const age = 18;

之前,JS 只能通过var声明变量,不过 ES6 引入了let。相对于varlet所声明的变量作用域更小,只在其所在的代码块内有效(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的判断语句就不展开讲了,简单来说ifif-elseif-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" == true0 == 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 引入的,当左侧参数为nullundefined的时候,则返回右侧参数:

let result = value1 ?? value2;

如果value1为为nullundefinedresult的值为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-infor-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()调用父类的构造方法,也可以用来调用父类的函数
比如Studentintroduce方法实际上等同于
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 {
  //···
}

参考

  1. JavaScript Algorithms and Data Structures
  2. 阮一峰:ES6 入门教程
  3. 【YouTube 百万粉丝大神 Mosh】JavaScript 系列完整教程(中英文字幕)
  4. JS 单引号、双引号与反引号的区别
  5. 逻辑运算符
  6. JavaScript String 对象
  7. JavaScript Array 对象