跳至主要內容

JavaScript简介和入门

xmut-lby大约 22 分钟

JavaScript简介和入门

JS是什么

JavaScript是一门主要用于浏览器的语言。 一般来说现代浏览器至少都会支持ECMAScript2015(ES6)这个版本,现在最新版本是ES2022或说ES13。

什么是ES ?

有时候你会看到ES5、ES6这样的简写,那么什么是ES?

ES是ECMAScript的缩写。

ES和JS其实是同一个东西,ES是JS的标准,JS是统称,所以如果看到ES5、ES6,应该懂得其实说的是JavaScript

一个直观的印象

打开任意一个页面,F12,在console或者终端里输入下面代码:

let s="Hello World!"
alert(s)

然后回车

页面会弹出一个窗口:

HelloWorld!
HelloWorld!

从这个简单的例子可以一窥js的特点:

JS的特点

JS是解释型语言

也就是说你输入的每一条语句都会立刻执行。

你们学过的C语言是所谓的编译型语言,程序要先被翻译成机器代码(可执行文件),然后再执行。 翻译的这个过程称为编译,执行编译的程序称为编译器(那么你知道Visual Studio/CodeBlock/DevC++这些开发环境的编译器是什么吗?)。

所以如果你出现了语法错误,你的代码一条都不会执行——逻辑是非常清晰的: 语法错误会导致编译过程失败,可执行文件也就无法生成,当然一条语句也无法执行。

JS是由浏览器负责解释并执行的。所谓console不仅是一个解释器的窗口,也是一个很好的调试窗口。比如你可以再输入:

console.log(s)

在我的Firefox上,可以看到输出是:

Hello World!

你可以直接打出该变量的值。这在调试的时候会非常有用。

编译型,解释型有什么区别?

这个知识真的是计算机领域最基础的概念性问题,请你自行百度。 我这里给出几个简单的差异:

解释型语言编译型语言
是否需要构建一般不需要需要
机器直接执行不行可以
执行速度慢1-3个数量级

为什么要用解释性语言?

从上面的表里我们可以看出,解释型语言的优点好像不是很突出,但在这点上的缺陷可以说非常显眼、令人难忘。

实际上你可以把编译型语言的执行速度就当作机器的执行速度。 因为对于我们常见的编译型语言,如C/C++/Rust等,编译后得到的代码不见得比手写机器代码慢——编译器比你懂机器!

而解释型语言的速度差别通常很大。对于Java这种用字节码解释运行的语言,如果解释器的JIT性能(和运气)足够好,是可以达到与编译型语言基本相同的速度的。

但对于Python,JS这类对JIT不友好的弱类型语言,速度可能会慢1-3个数量级。

那么我们为什么还要用呢?因为编译型语言的是有代价的:

代价是什么?
代价是什么?

这个代价就是程序员的头发——也就是说,开发的难度和工作量

编译型语言发现错误、调试错误、修改错误的工作量远大于解释型语言:

  1. 编译型语言的运行和代码是分开的,你看到的错误现象并不会直接对应到代码上;反之解释型语言一旦执行错误,立刻会在现场中断。
  2. 编译型语言的运行无法被打断和跟踪,所以你需要特别的工具——调试器,并编译特殊的调试版本来协助你寻找错误; 而解释型语言虽然也可以依赖调试器,但实际上大多数情况下直接打印变量值会更方便。
  3. 修改代码后,编译型语言需要重新编译,而解释型语言则不需要。编译过程会拖慢debug的效率——极端的情况下,重新编译的时间足够你泡杯茶再刷两部剧。

综上其实就是一句话:编译型语言的开发难度一般远大于解释型语言。

在软件开发的世界里,开发速度和运行速度一般是跷跷板的两端,不能两全。所以这就是要运行速度还是要程序员头发的选择问题。

当然这种取舍一般只在项目比较小的情况下有效。所以解释型语言(有时候叫脚本)通常被用来处理一些较低工作量的工作。

JS变量不需要申明类型

这应该是最令C语言程序员震惊的一点,JS里的变量没有类型。

let s = "Hello World!"

这里的let并不是某种类型(像C语言一样),而是告诉JS解释器,这里的s是定义的变量。

这里一定要转变观点,JS的程序是一边跑一边确定的,对于一个变量,我们赋予他什么类型,他就会是什么类型。

我们可以用typeof来看变量的类型:

>>let s="Hello World!"
undefined
>>typeof(s)
"string"
>>s=11
11
>>typeof(s)
"number"
>>s=1.1
1.1
>>typeof(s)
"number"
>>s=true
true
>>typeof(s)
"boolean"
>>s=function(){ consolg.log("hello world!");}
function s()
>>typeof(s)
"function"

>>表示控制台窗口的命令提示符,该行是用户输入(不包括>>)。 自己试一下就知道JavaScript有多神奇。

弱类型语言

类似JavaScript这样变量类型不需要明确给出(或者说变量类型可变)的语言称为弱类型语言

显然编译型语言是无法做到的,一般编译型语言都要求变量类型必须明确给出(或者更精确的说:变量类型必须可以在编译期被确定)。 这种语言被称为强类型语言

一般来说,语言越高级,就越倾向于弱类型。越低级就越需要强类型。常见的解释型语言基本都是弱类型的,除了Java。

但弱类型语言最大的问题在于解释器的优化难度比强类型语言难,JIT效果不明显,所以这类语言性能都比较差。

实时交互

很多解释型语言都有支持通过命令行进行实时交互,也就是说你输入一条语句,就立刻给你这个语句的反馈。

对于JavaScript,这个交互命令行就是在上面出现的网页控制台。

当然非常明显,只有解释型语言才可能提供这种实时的交互。

嵌入页面

我们使用JS当然不是为了在控制台里进行交互的,而是要跟页面交互的。那么我们就需要让js可以嵌入页面中执行。 这里有两种方法。这两种方式和css类似,一种是直接嵌入html里,另一种外部引入。

嵌入html

js可以用script标记来嵌入js代码:

<script>
    alert("页面加载完毕");
</script>

代码可以放在页面的任意一个位置,浏览器加载到这个块就立刻执行。 把这段代码放入页面,当你打开页面就会弹出一个对话框。

通过外部js文件引入

外部引入的方式也和css类似。先写一个js文件,这里命名为main.js:

alert("页面加载完毕");

在页面需要插入的地方这么写:

<script src="main.js"></script>

试一下,你可以看到结果。

最基本的语法

经常有人会问,JavaScript和Java什么关系?答案是就像老婆和老婆饼的关系。

某种意义上JavaScript会更接近C语言的语法。

如果你熟悉C语言,那不应该看不懂JavaScript的语法。

我们的课程上不会再对JS的基础语法一一进行详细介绍,只说明一些需要注意的点。我认为你应该明白为什么。

本章后续会通过一个作业代码来告诉让你对JS有一个比较介绍js语法里非常小的一部分——足够你完成今天的作业的部分。

大小写

JavaScript区分大小写,这和C语言一样(与HTML不一样)。Hello和hello是不同的两个变量。因为HTML不区分大小写,因此当两者产生关联时,需要小心。 当然,这点和C语言是一样的

变量命名规则

如果你C语言很熟,你应该知道C语言的变量只能有下划线、字母和数字。其中只能以下划线,字母起始。JavaScript与其类似。但JavaScript多了一个字符,JavaScript里允许变量名中出现美元符号$,$也可以放于变量名头部:

let $scope=0; //合法,$是合法的字符

这是JS和其他语言很不一样的一点。

不过,$符号一般会被JavaScript的框架库保留用来作为框架变量。除非你编写的是框架,否则建议不要使用这样的命名规则。

需要注意的是,JavaScript所谓的字母和数字,包含所有Unicode字符集里的字母和数字,比如:π\pi,你能打出来,就能用。

不过还是一样的,出于兼容性的考虑,建议你不要这么特立独行。

保留字

和所有语言一样,JavaScript也有一些保留字,保留字不能作为变量名,会引起歧义。这些保留字和C很类似,同时vscode里也会以高亮形式显示,你不应该弄错。

有哪些保留字,请看这里open in new window

但和C语言不同的是:JS不仅仅是一种语言,他还是一种运行环境!为了维持这些运行环境,JS必须预留很多的全局变量和函数供用户使用

使用这些名字做你的变量名不会产生语法问题,但很可能产生运行问题。举例来说,打开任意一个页面,输入window,你会发现有值:

>window
Window {window: Window, self: Window, document: document, name: '', location: Location,}

window是指向当前窗口的一个全局变量。

具体有哪些,每个浏览器和实现方式可能都不同,但总体还是有一些的,我们遇到再讲。

你说的我不明白啊。

要理解这点,我们可以从最基本的hello world说起。 一个最简单的C语言的hello world,一般是这样:

#include <stdio.h>

int main(){
    printf("hello world!\n");
    return 0
}

我相信很多人都没有对这样的代码认真琢磨过,只是有一个懵懵懂懂的概念——要打印信息,就调用printf函数,但你想过吗:

  1. printf函数是谁写的?
  2. 为什么printf可以打印信息?
  3. #include <stdio.h>有什么用,为什么需要?
  4. 只要#include <stdio.h>就可以跑了吗?

等等,如果你能回答上述的问题,那么你对C语言的理解是比较充分的。这个代码虽然简单,但其实内部非常不简单,充分体现了计算机这门学科的魅力:

计算机科学的一切都构建在坚实的基础之上,而且这些坚实的基础都是你可以理解的!open in new window

这个问题我可以小做回答:

  1. printf这个函数是所谓的C语言运行时
  2. C语言运行时是由专门的库(glibc,msvcrt)提供的,而这些库通过调用操作系统的系统调用来实现打印等功能。
  3. C语言编译器需要知道(注意!是知道,不是实现)存在一个叫printf函数。所以你需要引入stdio.h文件,里面有这个函数的声明。
  4. 引入stdio.h只是让编译器可以顺利编译,但你的程序在组装时,还需要把这个函数完整地放进你的程序。 这个工作由链接器完成,链接器会在指定的库里搜索这个函数并放到你的程序里(静态链接时),保证你可以正确调用这个函数。

不要以为打印一行Hello World!很简单,你觉得简单只不过是编译器、链接器、运行时、操作系统帮你负重前行了!

那么在JS里,并不需要这些东西,因为浏览器本身就是一个运行环境,本身就已经(必须)带一系列标准库。

例如最前面的alert函数,又或者当你需要在控制台打印调试信息时,你可能会用console.log函数:

console.log("hello world!")

alertconsole.log虽然不是JS的关键字,但却是JS运行环境的一部分。 不难想象如果你修改了他们(为什么JS能够修改写好的函数,而C语言不行呢?),你的代码没有语法错误,但跑起来肯定会出问题。

也就是说,用关键字做名字会导致语法错误,而使用这些预设的函数和全局变量,可能会导致运行期错误

那么你能区分这两者吗?

成员函数

没有学过面向对象的一般初学者可能会对console.log("Hello World!")这种用法比较陌生,不过这种方式其实也是很容易理解的。

我们可以看这样的C语言代码:

struct NODE{
    int value;
    struct NODE* next;
};

struct NODE node;
node.value = 200;

这里我们定义了结构体变量node,然后用node.value使用结构体变量的value成员变量(或者从面向对象的术语,称为属性)。 这应该是你们已经掌握的内容。那么只需要稍微扩充一下,既然可以定义成员变量,也应该可以定义成员函数:

struct NODE{
    int value;
    struct NODE* next;
    void print(){
        printf("%d\n", value);
    }
};

struct NODE node;
node.value = 200;
node.print(); // 预期这里应该输出200



 
 
 




 

这应该是比较容易理解的。实际上真实世界里也是这样处理的,比如ObjectC、C++还有Java,语法和结果都是类似的。

那么类比到console.log上也应该容易理解,console是一个对象,这个对象里包含一个log的函数。

后续我们会遇到很多类似的情况,比如document.getElementById,就是调用document里的getElementById函数。

代码示例

这里给出一个简单的例子。 这个页面和对应的代码可以让图中的一个图片从左上角斜向向下移动,当图片接触到窗口窗口边缘时,就停止移动。

我们通过阅读这个代码来给出你完成作业所需的最小知识。

代码

先看js代码:

let xPos, yPos;
xPos = 0;
yPos = 0;
let step = 2;
let interval=0;
let block = document.getElementById("award"); //1.获得id=award的块
block.style.top = yPos.toString()+"px";

function changePos() {
    let width = document.body.clientWidth; //2. 获取窗口尺寸。
    let height = document.body.clientHeight;
    let Hoffset = block.offsetHeight;   //4.设置位置
    let Woffset = block.offsetWidth;
    block.style.left = xPos.toString()+"px";
    block.style.top = yPos.toString()+"px";
    yPos = yPos + step;  //5.根据步长调整位置
    xPos = xPos + step;  //5.根据步长调整位置

    if (yPos >= (height - Hoffset)) { //6.清除定时器
        clearInterval(interval);
    }
    if (xPos >= (width - Woffset)) {
        clearInterval(interval);
    }
}
interval = setInterval(changePos, 5);//7.开始定时器

变量定义、全局变量和局部变量

代码的这些部分都在进行变量的定义。

let xPos, yPos;
// ....
let step = 1;
let interval=0;
// ....

let block = document.getElementById("award");

//....
function changePos() {
    let width = document.body.clientWidth;
    let height = document.body.clientHeight;
    let Hoffset = block.offsetHeight;
    let Woffset = block.offsetWidth;
    //....
}

这里既可以先申明变量,也可以在申明变量的同时完成对变量初值的赋值:

let xPos, yPos; // 只申明变量

let step = 1; // 申明变量的同时给出初值

同时你应该注意到,有些变量是定义在函数外(xPosyPosstep等)的,有些则定义于函数内(widthheight等)。 这些变量拥有不同的作用域,函数外的变量是全局变量,所有函数都可以访问;函数内定义的变量则只有本函数能够访问。

这些规则和C语言是完全一致的。

还记得吗?

全局变量和局部变量是可能重名的。如果重名了怎么办呢?

还记得前面讲过的就近原则吗?当然这点与C语言也是一致的。

我们这里使用全局变量的原因在于这个函数会被多次调用(每5毫秒1次),我们需要一个全局变量来记录状态。

函数定义

接下来我们可以看到对函数的定义:

function changePos() {
    // ...
}

这里需要注意一点,function并不是这个函数的返回值!仅仅只是用来说明这里进行了函数的定义。

你是否会有这个疑问?

不知道返回值我怎么写代码啊!?

然而别忘记,你申明变量的时候也没说他是什么类型啊。

再次提醒,请抛弃对编译型语言的刻板印象,写函数不需要说明返回值的类型——而是你想要他返回什么类型,就可以返回什么类型

function test_func( flag ){
    if( flag==1 ){
        return "Hello World!"; // 函数返回String
    }
    else if( flag==2) {
        return 1;  // 函数返回Number
    }
    else if(flag=="3"){
        return;  // 这里返回null
    }
    // 这里也返回null
}

如果你不给出函数的返回值(也就是类似C语言里的void),那他返回的就是null

从上面的例子你们也可以看出,函数是可以带参数的,同样的,这个参数不需要给出类型,而且你传递给函数什么类型,他就是什么类型。

回调函数

这个函数我们并没有在函数内直接调用,而是在最后一句话里提了一下:

interval = setInterval(changePos, 5);//7.开始定时器

看上去我们是将函数作为参数传递给setInterval这个函数。这里实际上有两个问题:

  1. 函数也能像变量一样赋值和传参吗?
  2. 这个函数我们没有调用,那么它为什么可以跑?

第一个问题对大多数语言(包括编译型语言)都是成立的。实际上C语言的函数也是可以赋值的,只不过你不知道如何写它的类型罢了(了解一下函数指针)。

第二个问题涉及JS这个语言的一个核心概念,即回调函数

我们用一个比较简单的例子来讨论:

回调的例子
<h1 onclick="sayhello()">点击我看效果</h1>
function sayhello(){
    alert("我被点到了!");
}

在html的h1上,我们增加了一个新的属性onclick,其值等于"sayhello()"也就是调用这个函数。

于是当你点击h1元素时,会触发这个函数的调用,弹出对话框。

那么谁调用了sayhello这个函数?它并没有被显式调用。显式调用的意思是,我们并没有在js代码里直接调用他。他作为一个值被传递给html标记,当h1被点击时,这个函数会被调用。

我们把这种写出来不是自己用的,而是交给别人,让别人在合适的时候调用的函数称为回调函数。回调函数可以说是99%JS代码的基础。

要理解为什么需要回调函数,就必须理解网页UI的运行逻辑。 C语言初学者的处理逻辑是:我自己写main函数,我跑起来等用户输入(scanf),用户输入之后,我根据用户的输入给出我的结果。

但对于网页等GUI界面,一般你是无法轮询用户输入的,因为即便用户没有输入,内容也必须被渲染,运行逻辑不能停。 所以掌握用户输入信息的是浏览器或者系统的GUI框架。但我们又确实需要在用户执行一些操作时,做出响应。怎么办呢?

浏览器(或者说W3C规范)考虑到了这点,于是他提供一个接口,或者说约定,来给提供这种服务。

<h1 onclick="sayhello()">Hello World</h1>

这行代码里,onclick是双方约定的属性,用户填写了这个属性,给我这个函数,浏览器保证在用户点击的时候,就会运行这个代码。这种约定,保证了用户可以定制自己希望的响应。

为什么回调函数的概念重要

这个问题有两个回答:

  1. 从JavaScript的角度:事实上你可以认为JS就是在写各式各样的回调函数,JS是一种完全基于异步操作的语言。而异步操作的核心必须依赖回调函数
  2. 从软件工程的角度:事实上你们没有机会从main函数开始写,大部分都在框架下工作,而在框架下工作就是写各种各样的回调函数——没错,你们以后要学的Java多态,也可以视作一种广义的回调函数。

不理解?把你用EasyX写的C语言课设,用Windows API重写一遍,你应该能明白。

我们后面还会进一步讨论回调函数,回调函数在JS中处于非常核心的位置。

页面元素操作

我们在页面上写JS当然不会是为了弹出对话框或者在终端输出这么简单的,而是要跟页面元素进行互动,这也是他最强大的地方。在此之前我们必须先了解什么是页面的DOM结构。

所谓DOM,就是(Document Object Model) ,文档对象模型。

所有页面我们都可以将其结构化为树状结构。比如下面的代码:

<html lang="en">
<head>
    <title>Document</title>
</head>
<body>
    <h1>Hello World</h1>
    <a href="#"></a>
    <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
    </ul>
</body>
</html>

就形成类似的树状图,根节点是html

JS的DOM操作就是要在这棵树上找到对应的节点,并操作他。

我们看下具体的代码:

//....

let block = document.getElementById("award");
block.style.top = yPos.toString()+"px";
block.style.left = xPos.toString()+"px";

function changePos() {
    //...

    block.style.left = xPos.toString()+"px";
    block.style.top = yPos.toString()+"px";
    //....

}
//....

这里有几个部分可以帮助大家理解JS是如何与页面元素交互的。首先我们要获得页面的元素:

let block = document.getElementById("award");

这里要注意一下document这个变量,就是我们上面说的运行环境的一部分。document就是当前页面的所有内容。

这行代码会获取id名为award的页面元素。也就是页面的这块:

<div id="award">
    <img src="images/award.gif"
        alt="Roaster of the Year award" />
</div>

这样我们就将block这个变量和这个块元素建立了关联,后续我们对block属性的修改就可以反应在页面的块元素上。

在我们的这段代码里我们主要通过修改block.style下面的topleft属性。 这里block.style对应block这个块的css属性。 或者更准确的是,修改的是blockstyle属性。例如我们如果给block.style.top赋值100px。 那么相当于将对应的块修改为:

<div id="award" style="top:100px">
    <img src="images/award.gif"
        alt="Roaster of the Year award" />
</div>

所以下面的代码都是修改这个块的topleft,其实也就是修改这个块的位置(记得吗?绝对定位)。

理解了这些原理,那么对照着代码和注释我们就很容易理解代码是如何运行的了:

let xPos, yPos;
xPos = 0;
yPos = 0;
let step = 1;
let interval=0;

let block = document.getElementById("award");//1.获得id=award的块
block.style.top = yPos.toString()+"px";
block.style.left = xPos.toString()+"px";

function changePos() {
    let width = document.body.clientWidth; //2. 获取窗口尺寸。
    let height = document.body.clientHeight;
    let Hoffset = block.offsetHeight; //3. 获取块的尺寸
    let Woffset = block.offsetWidth;
    block.style.left = xPos.toString()+"px"; //4.设置位置
    block.style.top = yPos.toString()+"px";
    xPos = xPos + step; //5.根据步长调整位置
    yPos = yPos + step; 

    if (yPos >= (height - Hoffset)) {
        clearInterval(interval); //6.清除定时器
    }
    if (xPos >= (width - Woffset)) {
        clearInterval(interval); //6.清除定时器
    }
}

interval = setInterval(changePos, 5);//7.开始定时器
  1. 用document是全局变量的属性getElementById可以根据id获取获取award,也就是奖杯对应的块。接下去的一句,给block设置css属性top,这句话的作用相当于写top:0px;,因为此时yPos=0,我们通过toString将数字转成字符串,然后加上px,结果就等于0px,把这个值赋值给block.style.top,相当于为block设置css:top:0px
  2. 函数内部第一句,我们通过document.body.clientWidthdocument.body.clientHeight获得窗口尺寸,注意是窗口尺寸,跟滚动条无关。
  3. 接下来获取块的尺寸。
  4. 根据xPosyPos设置块的新位置,如果上面的1能看明白,那么这里是一样的。
  5. 根据步长,把xPosyPos移动到新的位置。
  6. xPosyPos有任意一个接触到页面边缘,就停止计时器。interval在下面的7里定义。
  7. 设置一个定时器,定时器的第一个参数是一个回调函数,第二个参数是间隔时间,每次间隔时间到了,第一个参数的回调就会被调用。间隔时间单位是毫秒。因此这句话的意思是每隔5毫秒,调用changePos()这个函数。setInterval会返回一个数字,这个数字传递给clearInterval则定时器会被停止,否则就一直执行下去。

从这个例子里我们可以看到js如何操作DOM的,简单说就2步:1. 找到这个DOM对象。2. 修改这个对象的属性来改变这个对象。

现在请你让这个块在页面里弹起来。