JavaScript入门| 青训营

79 阅读16分钟

JavaScript入门

什么是JavaScript

JavaScript是什么 🐱‍👓

如果说HTML是网页的骨骼、css是网页的皮囊,那么JavaScript就是整个网页的灵魂,它驱动着整个网页与用户的交互。它可以写在 HTML 中,在页面加载的时候会自动执行,不需要特殊的准备或编译即可运行。所以JavaScript是一种运行在浏览器中的解释型的弱类型的编程语言。

JavaScript的实现 🧩

JavaScript常常和ECMAScript同时出现在我们的视野中,这是因为这俩基本就是同义词。但是JavaScript却不限于ECMA-262 所定义,完整的JavaScript包含以下几个部分:

  • 核心(ECMAScript)
  • 文档对象模型(DOM)
  • 浏览器对象模型(BOM)

img

为什么需要JavaScript

对于为什么前端程序员需要JavaScript,原因可以概括为以下几点:

  • 所有设备中的网页都由JavaScript驱动完成。
  • 只有Javascript能跨平台、跨浏览器驱动网页,与用户交互
  • 在JavaScript中能够嵌入动态文本到HTML页面中,也能读取HTML页面内容。
  • 数据可视化、页面内容实时更新,交互式地图,2D/3D动画,滚动播放音视频等等,都可以由JavaScript实现。
  • JavaScript的掌握程度很大程度上决定了前端程序员的定位

进入JavaScript的世界

javascript世界的规则 📋

在JavaScript的世界中,也有着相应的规则,每一个前端程序员都应该尽力去遵守,良好的编程习惯会让人受益匪浅。

区分大小写

ECMAScript中的一切都区分大小写,无论是变量、函数名还是操作符,我们在命名时应当有意去进行大小写区分,如构造函数名的首字母和类名的首字母习惯大写等等。

标识符的规范

标识符就是变量、函数、属性或者函数的名称。标识符可以由一个或多个下列字符组成:

  • 第一个字母必须是字母、下划线(_)或美元符号($)。
  • 剩下其他的字符无特殊要求

在JavaScript的世界里,长串的标识符使用驼峰命名法来进行命名更为推荐,即第一个单词的首字母小写,后面每个字母的首字母大写,如:firstDay、myCar...

同时关键字(如break、typeof、instanceof、new...)、保留字(public、private、let...)、true、false和null都不能作为标识符

注释

优秀的前端程序员的代码通常都是可读的,所以使用注释的方法让代码可读至关重要。

js的注释采用c语言的注释风格,单行注释以两个斜杠开头,多行注释以一个斜杠一个星号开头,一个星号一个斜杠结尾。

//	单行注释
/*
    多行注释
    多行注释
    多行注释
*/

tips:vscode中鼠标选中要注释的代码,同时按住CTRL和 / 即可一键注释(部分编译器可能不同)

结尾分号

JavaScript的世界中,所有的语句都以分号结尾。虽然分号结尾不是必须,就算不写分号解析器也会自己确定语句在哪里结尾并隐式补上分号,但是却强烈建议遵守这个语法。原因如下:

  • 部分情况不加分号代码可能会和本意有出入。
  • 多条语句写在同一行不加分号会报错

使用分号的好处在于:

  • 加上分号可以避免很多错误,开发人员可以放心的通过删除多余的空格来压缩代码;
  • 加上分号在某些情况下可以增进代码的性能,因为这样解析器就不用花多余的时间去推测在哪里添加分号了。

变量声明三兄弟 👦

JavaScript是类型的一门语言可是名不虚传的,变量可以用于保存任何类型的数据。在JavaScript的世界中,有三个关键字可以声明变量:varletconst

img

var

语法:var+变量名

var text

​ 这样就定义了一个名为text的变量,可以用它保存任何类型的数据。但是值得注意的是,在变量未赋值的情况下,变量会默认保存一个特殊类型的数据 undefined 。

在变量声明之后,就可以进行变量的初始化。

var text = 'firstTest';
text = 13;  //合法,但是在js中修改变量保存值类型的操作是不被推荐的

​ 在这个例子中,text首先被初始化为'firstTest'的字符串,然后又被重写为一个13的数值,虽然有效,但是变量的数据类型发生了改变,是不推荐的。

var的声明提升

使用var时,变量的声明会自动提升到作用域的顶部。

function foo(){
	console.log(age);  //undefined
  var age = 21;
}

显然,变量age出现了变量提升,因为如果不提升,那么在打印age时就会报错,但是奇怪的是为什么打印的结果是undefined呢?原因在于,提升的只有变量,而没有赋值操作。下面的代码和上面等价。

function foo(){
	var age;
  console.log(age);  //undefined
  age = 21;
}

let

语法:let+变量名

let和var的作用差不多,都是用于声明变量,但是有几点主要的区别。

1、let声明的范围是块级作用域,而var声明范围是函数作用域。

if(true){
    var name = 'Billy';
    console.log (name);  //Billy
}
console.log (name);  //Billy
if(true){
    let age = 20;
    console.log (age);  //20
}
console.log (age);  //Uncaught ReferenceError: age is not defined

​ 在这里,if外部的age不能被打印出来,原因就是let声明的变量被限制在了if块内,而var没有这样的限制,所以在外部能正常输出信息。

const

语法:const+变量名 = 初始化的值

const和let基本相同,有一个重要的区别就是变量必须初始化,并且修改使用const声明的变量的值会报错。

const age = 26;
age = 36;  //Uncaught TypeError: Assignment to constant variable.

const name;  //Uncaught SyntaxError: Missing initializer in const declaration

​ 使用const声明的变量一定是个常量,常量的值不可修改。

数据类型大家族 👨‍👩‍👧‍👦

在JavaScript的世界中,一共有7中基本数据类型:Undefined、Null、Boolean、Number、string、Symbol和BigInt,还有一种引用数据类型Object。这八种数据类型撑起了整个JavaScript数据领域的天空,相比于其他一些编程语言五花八门的数据类型,js的数据类型还是相对友好的。

Undefined

undefined,字面意思就是没有定义,很好理解。Undefined这个类型只有一个值,就是undefined。(Undefined以类型出现,首字母大写,以值出现不用,后面的数据类型同理)

当使用var和let声明了变量而不初始化时,相当于默认给变量赋予了undefined。

let message;
console.log(message);  //undefined

Null

Null这个类型也只有唯一值null。null值表示一个空对象,使用typeof进行类型检测null时,会返回Object也就可以这么理解(但是事实上这是一个从JavaScript第一版一直遗留下来的bug🐛,基本数据类型是不应该返回引用数据类型这个结果的)。

let name = null;
console.log(typeof name); // object

Null有一个重要的作用,在定义将要保存对象值的变量时,可以先设置为null,null表示一个空对象(前面提到过),这样只要检查这个变量的值是不是null就可以知道这个变量有没有被重新赋予一个新对象的引用。

Boolean

Boolean有两字面量:true和false。Boolean在js中使用非常频繁

Number

数据类型家族中的Number可就非常有意思了,Number类型使用IEEE754格式表示整数和浮点数,在js中,几乎所有的数字都用Number表示(BigInt没出现之前),省去了其他语言众多数据类型的繁琐,像啥int、float、long、short这些都没有。

有一个特殊的数值是NaN,表示"Not a Number",字面意思就是不是数值,用来表示本来要返回数值的操作失败了的返回值。

console.log(0/0)  //NaN

String

String数据类型表示多个16位Unicode字符序列。可以使用双引号(" ")、单引号(' ')或者反引号( )都是合法的。

const name = "Billy";
const val = 'test';
const gender = `male`;

​ 但是使用不成对的引号会抛出语法错误。

const name = 'Billy`  //Uncaught SyntaxError: Invalid or unexpected token

​ 字符串是不可变的,一旦创建了,它的值就不会再改变,若要修改这个变量的字符串值,就会先销毁原始字符串,再保存新值。

ES6中新增加了模板字符串的使用,这意味着字符串变得更加的灵活。模板字符串使用反引号 (``) 来代替普通字符串中的双引号和单引号,并可用${}将变量或表达式括起来。

Object

ECMAScript中的对象是一组数据和功能的集合。对象通过new操作符后跟对象类型的名称创建。

变量们和它们的作用域 🌎

在JavaScript的世界中,变量是松散的,因此变量也不过只是一个特定称谓的名字。由于没有规则定义变量必须包含什么数据类型(像c语言就需要),所以你想让变量在任何时候变成任何元素都没有关系,这样对于开发者而言很轻松,也很危险。

原始值和引用值

ECMAScript中变量分为两类:原始值引用值。原始值就是简单的数据(基本数据类型),而引用值就是由多个值构成的对象(引用数据类型)。

值得注意的是,保存原始值的变量是按值访问的,我们操作的就是存储在内存中的实际值,而保存引用值的变量是按引用访问的。学过c++等类似其他编程语言的同学可能会有疑问,访问对象不是应该访问的是地址吗?原因在于,在JavaScript的世界中,禁止我们直接访问内存地址,因此也不能直接操作对象所在的内存空间。

复制一个值

原始值和引用值除了存储的方式不同,在进行复制的时候的动作也有所不同。

在将一个原始值赋值于另一变量时,原始值会被复制到新变量的位置,这两个变量完全独立,互不干扰。

let num1 = 1let num2 = num1;

它们在内存中的变化过程可以这样理解:

img

但是在将一个变量的引用值赋值给另一变量时,存储在变量中的值也会被复制到新变量所在的位置,不同的在于,复制的值是一个指向原变量的指针,因此两个变量实际上指向同一个对象。当一个对象发生改变时,另一个对象也会发生变化。

// 对象字面量新建一个空对象
let obj1 = {

}
let obj2 = obj1;
obj1.name = 'Billy';
console.log (obj2.name);  //Billy

明明只给obj1添加了name属性,为什么打印obj2会有输出呢,下面的过程图有助于我们去深入理解。

img

可以看出,两个变量共享一个对象,当向对象中添加name属性时,object发生更新,因此两个指向同一个object的变量同时同步更新。

不一样的参数传递

ECMAScript中所有函数的参数都是按值传递的。什么叫按值传递,按值传递的意思就是函数外的值会被复制到函数内部的参数中。有的小伙伴就会说,访问都有按值访问和按引用访问,传递参数为什么只有按值传递呢?JavaScript与其他语言不一样的地方就来了。

按值传递,值是直接复制到形参之中,而按引用传递参数,值在内存中的位置会保存在形参之中,因此函数内部的变化会影响到函数外部的变化(因为内外变量都指向同一个内存地址),这在JavaScript的世界里是不可能的,我们看下面的例子。

function test(a){
    a = a + 10;
    console.log (a);  //20
}
let a = 10;
// 调用函数,并将上一行声明的变量a传入函数。
test(a);
console.log (a);  //10

如果是按引用传递,那么函数内部打印的a和函数外部打印的a应该都是指向同一内存地址,即结果都应该为20,但是结果是内部的a为20,外部的a没变,函数内部的变化没有影响到函数外部,因此可以证明参数不是按引用进行传递的。

变量们的作用域

作用域的作用

什么是作用域?简单来说,作用域就是一个变量的作用范围,使用作用域可以防止内存泄漏、命名冲突等种种问题。我们需要牢记一点的是:

内层作用域可以访问外层作用域的变量,外层作用域不能访问内层作用域。

下面我们来看个例子:

function test(){
    const a = 'inner';
}
console.log (a);  //Uncaught ReferenceError: a is not defined

显然外层不能读取到函数内部变量,目前它在函数作用域中的变量是私有的,不会内存外泄。当然要读取到它也可以通过闭包等方法获取。

作用域的最主要作用就是隔离变量,在不同的作用域下同名变量不会有冲突。

全局、函数和块级作用域

作用域可以分为全局作用域函数作用域块级作用域

有以下几种情形变量拥有全局作用域:

  • 最外层函数自身和在最外层函数外面定义的变量
  • 不声明直接赋值的变量自动声明为拥有全局作用域(不声明的变量自动作为window对象的属性)
  • 作为window对象属性的变量。
function outerFn(){
    console.log ('outerFn in global');
    a = 1;
    
}
//调用全局作用域中的函数outerFn,若有输出则证明为全局.
outerFn();
let variable = 'outer';
console.log (variable);		
console.log (window.a);  //证明a属于window的属性,且window对象属性属于全局变量。

//全部正常输出
//outerFn in global
//outer
//1

全局作用域的弊端在于容易污染命名空间,导致命名冲突,尤其是在项目庞大的情况下。

函数作用域就是声明在函数内部的变量的作用范围,函数作用域中的变量就只有函数内部可以访问到(这类变量被称为私有变量),外界要访问需要使用闭包等手段。

来new一个对象吧 🐮

每当春节回家,还在总是为被七大姑八大姨问有没有对象而烦恼吗?学了本章内容,让你可以从0创建一个对象,再也不用为没有对象而烦恼了。

在JavaScript的世界中,几乎所有事物都是对象。Boolean(new定义)、Number(new定义)、String(new定义)、date、正则表达式...除了基本数据类型以外,其余的都是对象。

我们前面提到的对象,其实就是某个特定应引用类型的实例。实例可以通过new操作符后跟构造函数来创建(构造函数就是用来创建对象的函数,通常需要大写)。

let obj = new Object();

上面就是使用构造函数Object()创建对象的一种常见方法。

创建对象的方法

上面我们已经介绍了第一种创建对象的方式,包含上面那种,一共常见的有三种方式,建议牢记。

方法一、使用Object()构造函数创建
let obj = new Object();
obj.name = 'Billy';
obj.age = 21;
console.log (obj);  //{name: 'Billy', age: 21}

​ 上面我们就成功创建了一个对象,且对象的名字叫做Billy,年龄为21岁,大家可以根据自己的喜好创建对象。

方法二、使用对象字面量创建对象
let obj = {
    name: 'Billy',
    age: 21
}
console.log(obj);  //{name: 'Billy', age: 21}

​ 上面创建的对象和使用方法一创建的对象是一样的,值得注意的是使用对象字面的语法,多个属性之间应该使用逗号隔开。

方法三、使用构造函数创建对象
function Person(){
    this.name = 'Billy';
    this.age = 21;
}
let p = new Person();
console.log (p);  //Person {name: 'Billy', age: 21}

ES中的构造函数是用于创建特定类型对象的,如Object和Array这样的原生构造函数,可以直接使用。当然使用像上面案例中的自定义构造函数也是很常见的。使用构造函数创建的对象有明确的标识。

对象属性的增改查

属性添加

在JavaScript的世界中,对象的属性是灵活的,在对象中添加属性我们可以直接使用点 . 语法。

let obj = {
    name:'Billy',
    age:21
}
obj.gender = 'male';
console.log (obj);  //{name: 'Billy', age: 21, gender: 'male'}

​ 还有另外一种添加属性的方式,就是使用中括号 [] 语法。

const a = 'name'
let obj = new Object();
obj[a] = 'Billy';
console.log (obj);  //{name: 'Billy'}

开发人员在动态添加对象属性的时候,更倾向于使用点方法,因为它更简单。但是如果遇到需要使用非字符串的数据作为属性的话,就只能选择中括号语法。中括号中可以穿任何类型的数据,甚至传递一个对象都是合法的。因为传递的数据会被toString()方法进行转换。

let a = 'name'
let b = 1; 
let c = {
    name:"test"
}
let obj = new Object();
obj[a] = 'Billy';
obj[b] = 'test1';
obj[c] = 'test2';
console.log (obj);  //{1: 'test1', name: 'Billy', [object Object]: 'test2'}

​ 值得注意的是,如果使用中括号语法进行属性添加,那么读取时也必须要使用中括号语法。

属性修改

直接通过点语法同样能够实现属性的修改。

let obj = {
    x:1,
    y:2
}
obj.y = 10;
console.log (obj);  //{x: 1, y: 10}

​ 要修改的前提是这个属性原本存在,如果原本不存在那么就是实现的添加属性的操作。

属性查找

仍然是通过点语法和中括号语法。

let obj = {
    x:1,
    y:2
}
console.log (obj.y);  //2

本节主要提及的增删改查的方式主要是点语法和中括号语法,增删改查的方式当然远不止这些,还有

Object.getOwnPropertyNames()、Object.keys()、Object.getOwnPropertyDescriptor()等等方法,这些都等着我们在后面的学习中进行深入的探索。

Object的两个儿子 👨‍👦‍👦

我们在用typeof进行数据类型检测时,除了基本数据类型会返回基本数据类型的结果,按照前面我们在3.3节中所给出的八种数据类型的划分,难道其他的都是返回Object结果吗?其实不然,在检测函数(Function)时,它是独立的,虽然都是Object。我们看下面的代码。

//  基本数据类型
const num = 1;
let flag = true;
const str = 'string';
const u = undefined;
const n = null;
const s = Symbol();
const big = BigInt('99999999999999');
// 引用数据类型
let obj = {

}
// 创建一个函数
function test(){
    
}
// 创建一个数组
const arr = [];

console.log (typeof num);  //number
console.log (typeof flag);  //boolean
console.log (typeof str);  //string
console.log (typeof u);  //undefined
console.log (typeof n);  //object  (版本遗留问题)
console.log (typeof s);  //symbol
console.log (typeof big);  //bigint

console.log (typeof obj);  //object
console.log (typeof test);  //function
console.log (typeof arr);  //object

函数是一个对象,毋庸置疑,但是返回的结果却是function,这是因为函数属于对象子类型。在JavaScript的世界中,除了Function,还有另一个对象子类型,就是数组(Array)。

const arr = [];
console.log (arr instanceof Array);  //true

​ 显然数组也是有自己的类型的,因此这两种类型(Function和Array)都是对象子类型,我们可以把它们称为Object的儿子们。下面我们会详细介绍它们。

数组

数组是一组有序的数据,在JavaScript的世界中,数组可以存储任何类型的数据且数组可以动态添加,自动增长(太舒服了)。

创建数组

创建数组的常见方法有三种,一种是使用构造函数Array()来创建。

let animals = new Array(3);  //创建一个包含三个元素的数组
let cars = new Array('benz','BMW');  //创建一个包含两个元素的数组,分别保存字符串benz和BMW

还有一种方法是使用数组字面量。

let arr = ['benz','BMW',1,2,3];

​ 使用数组字面量的方式创建数组是使用的最多的,因为简单方便。

最后一种方式是使用ES6新增的方法from()来创建。

let arr = ['benz','BMW',1,2,3];
let copyArr = Array.from(arr);
console.log (copyArr);  //['benz', 'BMW', 1, 2, 3]

​ 可以看出,使用Array.from()实际上是通过传入一个可迭代对象或类数组,对一个可迭代对象执行浅复制来创建的,这种创建对象的方式不太常用。

数组空位

使用数组字面量初始化数组时,可以使用一串逗号来创建空位。

let a1 =[,,,,,];
console.log(a1.length);  //5
console.log(a1[0]);  //undefined

​ 在JavaScript的世界中,这种空位是被认可为存在的元素的,只不过值为undefined。

数组索引

要取得或设置数组的值,需要使用中括号并提供数组的索引。

let arr = ['red','yellow','blue'];
console.log (arr[1]);  //yellow

​ 中括号中提供的索引表示要访问的对应的值。值得注意的有两点:

第一点是数组索引在读取和设置时都是从0开始的,这一点对于很多第一次学习编程语言的同学来说会很不习惯,但是在其他编程语言如c、c++里面都是一样的。

第二点是如果写入的索引超过了数组的最大索引,那么数组长度会自动扩展到索引的位置,不会报错。这一点和别的编程语言就不同了。

let arr = ['red','yellow','blue'];
console.log (arr[10]);  //undefined
arr[0] = 'black;
console.log(arr)  //['black', 'yellow', 'blue']

​ 数组中的元素的数量保存在length属性中。

let arr = ['red','yellow','blue'];
console.log(arr.length);  //3

​ 使用length有个小技巧,就是通过它可以向数组末尾添加元素。

let arr = ['red','yellow','blue'];
arr[arr.length] = 'black';
console.log(arr);  //['red', 'yellow', 'blue', 'black']

检测数组

检测数组的方式有两种,可以通过instanceof操作符和isArray()方法来检测。

let arr = ['red','yellow','blue'];
console.log (arr instanceof Array);  //true
console.log (Array.isArray(arr));  //true

函数

函数是JavaScript中最有意思的部分之一,主要是因为函数也是对象。

函数的创建

我们可以使用四方式来创建函数,函数声明、函数表达式、箭头函数和Function构造函数。

在js中,使用函数声明的方式来创建函数是最频繁的。

语法:function 函数名(参数1,参数2){ 函数体 }

function sum(num1,num2){
    return num1+num2;
}
const res = sum(1,2);
console.log (res);  //3

另一种创建函数的方式是使用函数表达式来创建。

语法:let 变量 = function(参数1,参数2){ 函数体 }

//赋值操作右边的函数因为没有名称,所以我们称它为匿名函数
let sum = function(num1,num2){
    return num1+num2;
}
const res = sum(1,2);
console.log (res);  //3

​ 还有一种和函数表达式很相似的创建函数的方式,我们称为箭头函数。

语法:let 变量 = (参数1,参数2)=>{ 函数体 }

let sum = (num1,num2)=>{
    return num1+num2;
}
const res = sum(1,2);
console.log (res);  //3

​ 最后一种方法是使用构造函数,就和创建一个对象、数组相似。这种方式并不常用而且会影响性能。

let sum = new Function("num1","num2","return num1+num2");
const res = sum(1,2);
console.log (res);  //3

​ 使用最后一种方法会影响性能的原因在于创建的过程中代码会执行两次,第一次是将它作为常规代码,第二次是解释传给构造的字符串。但是把函数想象成对象,把函数名想成指针的思想非常重要,最后一种方法就是这种概念的经典诠释。

函数名

大多数的函数都是有自己的名字的,这个名字和函数的功能息息相关,没有名字的函数被称为匿名函数,箭头函数就是经典的匿名函数。

函数名是一个指向函数的指针,所以它跟其他包含对象指针的变量具有相同的行为。这也就意味着一个函数可以有多个名称。

function sum(a, b) {
    return a + b;
}
let copySum = sum;

let res1 = sum(1,2);
let res2 = copySum(2,3);
console.log ('res1=',res1);  //res1=3
console.log ('res2=',res2);  //res2=5
//就算将原来指向函数的指针置为空,但是函数仍然存在。所以副本函数还是可以运行。
sum = null;
console.log (copySum(5,6));  //11

img

函数的属性

每个函数都有两个属性,length和prototype。

prototype的知识前面我们已经讲过,而对于length想必大家都充满了疑惑,函数有啥length呢,不应该只有数组才有吗?确实函数中是由length的情况较为少见,它是用来保存函数定义的形参个数的。

function sum(a, b) {
    return a + b;
}
function test1(a) {
    return a;
}
function test2() {
    return;
}
console.log (sum.length);  //2
console.log (test1.length);  //1
console.log (test2.length);  //0

​ 函数的方法就不想数组那么多了,就只有两个:call和apply。用来转函数体内this的指向的。this会指向传入小括号内的对象。