抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

这一篇笔记主要记录下ES6版本的Javascript的入门学习笔记,只记录了自己了解的基础知识,实际用这门语言的时候还是要多查阅文档。

顺便推荐几个github上收藏量比较高的JavaScript学习仓库:

getify/You-Dont-Know-JS: A book series on JavaScript. @YDKJS on twitter. (github.com)

trekhleb/javascript-algorithms: 📝 Algorithms and data structures implemented in JavaScript with explanations and links to further readings (github.com)

airbnb/javascript: JavaScript Style Guide (github.com)

1. JavaScript基础

HTML中的JavaScript脚本代码必须位于 <script></script>标签之间,可以在<head>中,也可以在<body>中。

放在<head>中的脚本一般是定义一个JavaScript函数,后续在<body>中引用;或者也可以像前面的CSS一样从外部引入,需要在<script>标签的src属性中设置JS文件的吧位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- 一种引入JS的方式 -->
<script>
function myFoo() {
document.getElementById("test").innerHTML="修改HTML当前标签的内容";
}
</script>
</head>
<body>
<p id="test">这是一个id为test的段落</p>
<!-- 创建按钮并绑定单击事件,调用函数 -->
<button type="button" onclick="myFoo()">修改</button>
<!-- 外部引入JS的方式 -->
<script src="js/index.js"></script>
</body>
</html>

以上是个简单的例子,点击按钮后可以改变id属性为”test”的标签的内容,可以看到JavaScript是如何控制网页行为的。

JavaScript可以用console.log()的方式将输出数据写入控制台;或者用window.alert()的方式将输出结果弹窗显示,方便起见后面的例子都用console.log()将结果写入控制台(浏览器中按F12,点击控制台)。

我是python入门的,新学的编程语言也都会和python进行对比,JavaScript对缩进的要求不像python那么严格,因为python以缩进(4个空格)区分代码块,而JS以左右花括号区分代码块,换行的缩进建议为两个空格

在ES6版本之后,官方建议使用const(声明常量)let(声明变量)代替var进行变量声明和赋值,var作用域是函数(指函数内),而const和let是块级作用域(左右两个花括号之内),以下是作用域的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function example() {
// 使用 let 声明块级作用域变量
let x = 10;

if (true) {
// 在块级作用域内部声明新的变量,不会影响外部的 x
let x = 20;
console.log(x); // 输出 20
}

console.log(x); // 输出 10,外部的 x 不受内部影响

// 使用 const 声明常量
const y = 30;
// y = 40; // 错误,常量不能重新赋值

// 使用 var 声明变量
var z = 50;

if (true) {
// 在同一作用域内重复声明变量,不会引发错误
var z = 60;
console.log(z); // 输出 60
}

console.log(z); // 输出 60,外部的 z 受内部影响
}

example();

再说明下这里的var作用域是函数内,所以和python一样,一个函数内定义的变量名不会影响到其他函数(除非你在函数外定义,那作用域就是全局)。

不仅仅是作用域的区别,在JavaScript中有个很有意思的概念**Hoisting(声明提升)**。我们知道python是一种顺序执行的语言,从上到下一行一行执行代码,JavaScript某种程度上也是顺序执行,但是可以被打破。

举个例子,var以及函数声明会被提前到最近的作用域的最前面(const和let不会),但是赋值语句没有被提前,这就意味着我们可以在声明变量/函数之前使用变量/函数,但是变量的值就会成为undefined

1
2
3
4
5
6
7
8
9
10
11
12
13
14
console.log(x); // 输出:undefined,也就是说这个变量此时还未赋值,注意这不是报错
var x = 5;

foo(); // 输出:5
function foo() {
console.log(x);
}

// 用 const,let就不一样了。
example()
function example() {
console.log(y); // 输出:ReferenceError,这才是报错,引用在初始化(赋值)前
const y = 5;
}

在上面的示例中,变量 x 和函数 foo 的声明被提升到作用域的顶部。因此,即使在它们的声明之前使用,代码也不会引发错误。但是,变量 x 的值在声明之前是 undefined,而后才被赋值为5。还有一点,函数声明提升的优先级高于变量声明提升,这意味着函数声明会覆盖同名的变量声明。

为了不引起混淆,在使用变量和函数前都要先进行声明,对于变量的声明就用不会被提升的const和let。

2. 数据类型

上面列了些javascript和python带给我的直观区别,要入门一门语言还是要从最基础的数据类型开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// String, Number, Boolean, null, undefined, Symbol
const username = "Phantom";
const age = 26;
const iscol = true;
const x = null; // 值被定义,但是空,typeof显示类型为object(历史遗留问题)
const y = undefined; // 不存在定义
const n = Symbol(); // 表示独一无二的值,最大的用法是用来定义对象的唯一属性名
console.log(typeof x); // typeof查看类型(有风险,比如上面的null,以及声明提升的问题)

// null类型数据不是Object
const z = null;
console.log(z instanceof Object); // 注意大写的O,输出:false
console.log(z === null); // 输出:true

// Symbol确定对象属性名的唯一性,避免与其他属性冲突
const symbol1 = Symbol('description');
const symbol2 = Symbol('description');
console.log(symbol1 === symbol2); // 输出:false

3. 对象和解构赋值

要明确一点,JavaScript和python一样都是面向对象的编程语言。JavaScript的对象由花括号分隔,在括号内部,对象属性以名称和值对的方式来定义,属性之间逗号分隔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建一个对象
const person = {
name: 'Phantom',
age: 26,
hobbies: ["music", "sing", "dance", "basketball"],
address: {
street: "tarim street",
city: "tarim",
},
sayHello() {
console.log(`Hello, my name is ${this.name}.`);
}
};

// 调用对象的方法和属性
person.sayHello(); // 输出:Hello, my name is Phantom.
console.log(person.name, person.age); // 输出:Phantom 26

JavaScript中对赋值运算有个比较有意思的拓展,叫做解构赋值,可以对数组或者对象进行模式匹配,变量一个萝卜一个坑对应进行赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 数组解构赋值
let [a, b, c] = [1, 2, 3]
console.log(a, b, c) // 输出 1 2 3

// 对象解构赋值
const person = {
firstname: "Alex",
lastname: "Xie",
age: 18,
hobbies: ["music", "sing", "dance", "basketball"],
address: {
street: "tarim street",
city: "tarim",
},
};
const {
firstname,
lastname,
address: { city },
} = person; // 相当于用同名变量将值从person变量中抽取出来
console.log(city); // 输出tarim

在这里,我们通过解构赋值从 person 对象中提取了 firstnamelastnameaddress 属性,并将 address 属性解构为 city 变量。由于 person 对象的 address 属性是一个对象,而我们只关心其中的 city 属性,所以通过解构赋值的方式将 city 属性提取出来。

整个过程相当于用一个同名变量将值从person对象中提取出来。

4. 常用的内置方法

方法部分就比较多了,这里就列举常见的,真正需要的时候得查手册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 字符串内置方法
const username = "Phantom";
console.log(username.length); //.length 长度,没括号是属性 输出:7
console.log(username.toUpperCase()); // 有括号的是方法 输出:PHANTOM
console.log(username.substring(0, 3).toUpperCase()); // 输出:PHA
console.log(username.includes("P")) // 判断是否含有参数字符串 输出:true
console.log(username.startsWith("n")); // 判断是否参数字符串开头,注意函数名是驼峰写法 输出:false
const test_txt = "p h a n t o m";
console.log(test_txt.split(" ")); // 切分数组,其实和python的.split()方法是一样的 输出:['p', 'h', 'a', 'n', 't', 'o', 'm']

// 数组内置方法
const numbers = new Array(1, 2, 3, 4, 5); // 构造函数创建数组
const fruits = ["apple", "banana"]; //直接用中括号申明数组
console.log(numbers); // 输出:[1, 2, 3, 4, 5]
console.log(fruits[1]); // 中括号索引,和python一模一样 输出:banana

fruits.push("watermalon"); // .push() 数组中添加元素,等同于python列表的.append()
console.log(fruits); // 输出:['apple', 'banana', 'watermalon']
fruits.pop(); // 删除末尾元素
console.log(Array.isArray(fruits)); // 判断是否为数组 输出:true
console.log(fruits.indexOf("apple")); // 计算索引 输出:0

5. 条件语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// if条件语句
const n = 20;
const m = 10;
// ===才会连数据类型一起判断,==不会判断数据类型
if (n === 10) {
console.log("n is 10");
} else if (n > 10 || m > 10) {
// 条件判断中”或“用||双竖线表示,”且“用&&表示
console.log("n is greater than 10 or m is greater than 10");
} else {
console.log("n is less than 10 and m is less than 10");
} // 输出:n is greater than 10 or m is greater than 10

// 三目运算符(简化if else代码用)
const z = 11;
const color = z > 10 ? "red" : "blue"; // ?表示判断为真,冒号后是判断为假
console.log(color); // 输出:red

// 另一种条件语句switch
switch (color) {
case "red": // 需要注意case判断的条件是===
console.log("color is red");
break; // case break相当于shell里的if fi
case "blue":
console.log("color is blue");
break;
default: // 表示没有匹配到任意一个条件,执行下面得代码块
console.log("color is not red or blue");
} // 输出:color is red

6. 循环语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// for 循环
for (let i = 0; i < 10; i++) {
console.log(`for loop number: ${i}`); // 像shell里的for循环,注意符号``
} // 输出:for loop number: 0 一直到9,10行

// while 循环,与for不同,条件判断在while里面,否则死循环
let l = 0;
while (l < 10) {
console.log(`while loop number: ${l}`);
l++;
} // 输出:while loop number: 0 一直到9,10行

const array = [1, 2, 3]
for (let i = 0; i < array.length; i++) {
// 结合数组的for循环
console.log(array[i]);
} // 输出 1 到 3 共三行

for (let i of array) {
// 类似于python的for i in [list]
console.log(i);
} // 输出 1 到 3 共三行

同样有continuebreak 是用于控制循环流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// continue 跳过当前循环剩余代码,进入下一个循环
for (let i = 0; i < 5; i++) {
if (i === 2) {
continue;
}
console.log(i);
} // 输出:0 1 3 4(4行)

// break用于完全终止循环,跳出循环体
let i = 0;
while (true) {
if (i === 3) {
break;
}
console.log(i);
i++;
} // 输出:0 1 2(3行)

7. 类和构造函数

JavaScript也使用 class 关键字来声明类,类名通常使用大写字母开头。类的方法定义和python类的方法定义是一样的,都是使用普通函数的语法。

1
2
3
4
5
class MyClass {
myMethod() {
// 方法的定义
}
}

和python的构造函数使用方法名__init__()定义类似,JavaScript的构造函数是类的一个普通方法,使用 constructor 关键字定义。Python的构造函数可以接受任意数量的参数,包括 self(指向类的实例)作为第一个参数。在构造函数内部,可以使用这些参数来初始化实例的属性。而JavaScript的构造函数使用普通的函数参数来接收传递的值,没有特殊的self参数

演示一下两者构造函数的差异:

1
2
3
4
5
6
7
8
9
# python的构造函数
class MyClass:
def __init__(self, arg1, arg2):
self.arg1 = arg1
self.arg2 = arg2

my_instance = MyClass("Hello", "World")
print(my_instance.arg1) # 输出:Hello
print(my_instance.arg2) # 输出:World
1
2
3
4
5
6
7
8
9
10
11
// javascript的构造函数
class MyClass {
constructor(arg1, arg2) {
this.arg1 = arg1;
this.arg2 = arg2;
}
}

const myInstance = new MyClass("Hello", "World");
console.log(myInstance.arg1); // 输出:Hello
console.log(myInstance.arg2); // 输出:World

new关键字用于创建一个类的实例对象,比如上面的例子,通过new关键字,调用了MyClass类的构造函数,并且传递了参数"Hello" , "World",将构造函数的this绑定到新创建的对象,最后,new关键字返回了新创建的MyClass实例对象myInstance

constructor 关键字也不是一定要显示声明的,需要理解构造函数是用来创建和初始化对象的特殊函数,在 JavaScript 中,如果一个函数被用于创建对象,它就被认为是构造函数。

python的构造函数不允许显示返回值,只负责初始化实例状态。而 JavaScript 的构造函数可以显式返回一个对象,如果返回的是一个对象,则该对象将作为实例创建的结果,否则将返回新创建的实例(上面的例子就是)。

1
2
3
4
5
6
7
8
9
10
11
// 构造函数返回对象的情况
class MyClass {
constructor() {
this.property = "Value";
return { customProperty: "Custom Value" };
}
}

const myInstance = new MyClass();
console.log(myInstance.property); // 输出:undefined
console.log(myInstance.customProperty); // 输出:"Custom Value"

上面的例子中,构造函数使用了return语句返回了一个对象{ customProperty: "Custom Value" }。当我们使用new关键字创建MyClass的实例时,返回的结果不是新创建的实例对象,而是显式返回的对象,因此这个对象并没有 property 属性,而是具有 customProperty 属性。

8. 封装、继承和多态

既然JavaScript是面向对象的编程语言,就一定有面向对象的三大特征:封装、继承和多态。

8.1 封装(Encapsulation)

  • 对象和函数:很明显JavaScript中的对象和函数可以用来封装数据和行为。对象可以包含属性和方法,函数可以封装一段可执行的代码,并且可以接收参数和返回值。
  • 访问控制:通过使用闭包或者WeakMap,可以模拟私有属性和方法,从而实现对数据的隐藏和封装。

我们不能像python中一样用_或者__在属性或者方法上实现私有化,JavaScript提供了一些特殊的机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 使用闭包,简单来说,闭包是指一个函数能够访问并操作其作用域外部的变量
function MyFunction() {
let privateProperty = "Private Value";

this.getPrivateProperty = function() {
return privateProperty;
};

this.setPrivateProperty = function(value) {
privateProperty = value;
};
}

const myInstance = new MyFunction(); // 创建实例对象,执行构造函数代码,返回一个新的对象
console.log(myInstance.privateProperty); // 构造函数内部的局部变量,外部不可访问,输出:undefined
console.log(myInstance.getPrivateProperty()); // 输出:"Private Value"
myInstance.setPrivateProperty("New Value");
console.log(myInstance.getPrivateProperty()); // 输出:"New Value"

在上述示例中,我们使用闭包创建了一个私有变量privateProperty。通过在构造函数内部定义公有方法getPrivatePropertysetPrivateProperty,我们可以访问和修改私有属性(内部的两个方法访问修改了外部函数的变量)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 使用WeakMap
const privateProperties = new WeakMap();

class MyClass {
constructor() {
privateProperties.set(this, { privateProperty: "Private Value" });
} // WeakMap用法set(key,value)

getPrivateProperty() {
return privateProperties.get(this).privateProperty;
}

setPrivateProperty(value) {
privateProperties.get(this).privateProperty = value;
}
}

const myInstance = new MyClass();
console.log(myInstance.privateProperty); // 输出:undefined
console.log(myInstance.getPrivateProperty()); // 输出:"Private Value"
myInstance.setPrivateProperty("New Value");
console.log(myInstance.getPrivateProperty()); // 输出:"New Value"

MapWeakMap都是ES6中新的数据结构集,一个对象由多个key-value键值对构成,在Map中,任何类型都可以做对象的key;而在WeakMap中,key必需是对象,所有的key都是弱引用,不用考虑垃圾回收。

在构造函数中,使用privateProperties定义了一个WeakMap实例。通过 privateProperties.set(this, { privateProperty: "Private Value" }) 将当前实例对象作为键,将一个包含私有属性 privateProperty 的对象作为值存储在 WeakMap 中。

然后,通过在类的原型上定义 getPrivatePropertysetPrivateProperty 方法,访问和修改存储在 WeakMap 中的私有属性。

8.2 继承(Inheritance)

使用 extends 关键字来创建子类,并使用 super 关键字来调用父类的构造函数和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Animal {
constructor(name) {
this.name = name;
}

speak() {
console.log(`${this.name} makes a sound.`);
}
}

class Dog extends Animal {
constructor(name, breed) {
super(name); // 调用父类的构造函数
this.breed = breed;
}

speak() {
console.log(`${this.name} barks.`);
}

fetch() {
console.log(`${this.name} fetches the ball.`);
}
}

const myDog = new Dog("Buddy", "Golden Retriever");
myDog.speak(); // 输出:"Buddy barks."
myDog.fetch(); // 输出:"Buddy fetches the ball."
console.log(myDog.name); // 输出:"Buddy"
console.log(myDog.breed); // 输出:"Golden Retriever"

在上面的例子中,我们有一个父类Animal和一个子类Dog。子类Dog使用extends关键字继承了父类Animal(python就不需要)。子类Dog在构造函数中使用super(name)调用了父类的构造函数,并传递了name参数。

子类可以覆盖父类的方法,上面的例子就覆盖了父类的speak()方法。在子类中,我们可以使用this关键字引用当前实例的属性和方法,包括从父类继承的属性和方法。

8.3 多态(Polymorphism)

JavaScript 是一种动态类型语言,变量的类型可以在运行时改变。这种动态性使得 JavaScript 具有一定的多态性。例如,多个对象可以对同一个方法做出不同的响应,根据对象的实际类型来确定要调用的方法(相同接口,不同实现)。

举个例子,假设我们有一个 Animal 类,它有一个 makeSound 方法用于发出动物的声音。然后我们派生出两个子类 DogCat,它们分别重写了 makeSound 方法来发出不同的声音。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Animal {
makeSound() {
console.log("The animal makes a sound");
}
}

class Dog extends Animal {
makeSound() {
console.log("The dog barks");
}
}

class Cat extends Animal {
makeSound() {
console.log("The cat meows");
}
}

现在,我们可以创建不同的对象并调用它们的 makeSound 方法,它们会根据自己的实现发出不同的声音。

1
2
3
4
5
6
7
const animal = new Animal();
const dog = new Dog();
const cat = new Cat();

animal.makeSound(); // 输出:"The animal makes a sound"
dog.makeSound(); // 输出:"The dog barks"
cat.makeSound(); // 输出:"The cat meows"

本质上就是子类对父类的继承方法的重写,实现调用一个接口具有不同的实现。

对于第8部分可能有些难以理解,可以结合前面python的面向对象编程笔记:

python自学笔记(3)——面向对象编程(上) - 我的小破站 (shelven.com)

python自学笔记(4)——面向对象编程(下) - 我的小破站 (shelven.com)

JavaScript还有很多高级用法,这里只记录了入门的一些基础,以后要用到再深入了解。

欢迎小伙伴们留言评论~