JavaScript笔记

学习廖雪峰的JavaScript教程时写的笔记

简介

什么是JavaScript

JavaScript是世界上最流行的脚本语言,因为你在电脑、手机、平板上浏览的所有的网页,以及无数基于HTML5的手机App,交互逻辑都是由JavaScript驱动的。

JavaScript历史

1995年,当时的网景公司正凭借其Navigator浏览器成为Web时代开启时最著名的第一代互联网公司。由于网景公司希望能在静态HTML页面上添加一些动态效果,于是叫Brendan Eich这哥们在两周之内设计出了JavaScript语言

为什么起名叫JavaScript?原因是当时Java语言非常红火,所以网景公司希望借Java的名气来推广,但事实上JavaScript除了语法上有点像Java,其他部分基本上没啥关系

ECMAScript

因为网景开发了JavaScript,一年后微软又模仿JavaScript开发了JScript,为了让JavaScript成为全球标准,几个公司联合ECMA(European Computer Manufacturers Association)组织定制了JavaScript语言的标准,被称为ECMAScript标准。

最新标准:ECMAScript 6标准(简称ES6)

不同浏览器对标准有不同的实现,最流行的是Chrome的v8引擎,速度是最快的

一个完整的JavaScript实现应该由一下三个部分构成

  • ECMAScript::标准
  • DOM:文档对象模型,提供对象供我们操作网页
  • BOM:浏览器对象模型,提供对象供我们操作浏览器

JavaScript特点

  • 解释型语言,写完自己运行不用编译
  • 类似于CJava的语法结构
  • 动态语言,变量可以保存任意数据
  • 基于原型的面向对象

快速入门

JavaScript代码位置

  • 嵌在网页的任何地方,通常放到到<head>
1
2
3
4
5
6
7
8
9
10
<html>
<head>
<script>
alert('Hello, world');
</script>
</head>
<body>
...
</body>
</html>

<script>...</script>包含的代码就是JavaScript代码,它将直接被浏览器执行。

  • 把JavaScript代码放到一个单独的.js文件中,在HTML中通过<script src="..."></script>引入这个文件:
1
2
3
4
5
6
7
8
<html>
<head>
<script src="/static/js/abc.js"></script>
</head>
<body>
...
</body>
</html>

把JavaScript代码放入一个单独的.js文件中更利于维护代码,并且多个页面可以各自引用同一份.js文件。

  • 有些时候你会看到<script>标签还设置了一个type属性:
1
2
3
<script type="text/javascript">
...
</script>

但这是没有必要的,因为默认的type就是JavaScript,所以不必显式地把type指定为JavaScript。

基本语法(与Java、C++类似的不详细介绍)

  • alert('hello') 弹出窗口显示信息

  • document.write('hello') 在页面中输出一个内容

  • console.log('hello') 在控制台输出一个内容

  • 数据类型

    • Number:不区分整数和浮点数
    1
    2
    3
    4
    5
    6
    123;// 整数123
    0.456;// 浮点数0.456
    1.2345e3;// 科学计数法表示1.2345x1000,等同于1234.5
    -99;// 负数
    NaN;// NaN表示Not a Number,当无法计算结果时用NaN表示
    Infinity;// Infinity表示无限大,当数值超过了JavaScript的Number所能表示的最大值时,就表示为Infinity
    • 字符串:以单引号’或双引号”括起来的任意文本,如 'abc' 或者"abc"

    • 布尔值:truefalse

    • 简单的运算符:&&||!><

      • =====

      第一种是==比较,它会自动转换数据类型再比较,很多时候,会得到非常诡异的结果;

      第二种是===比较,它不会自动转换数据类型,如果数据类型不一致,返回false,如果一致,再比较。

      不要使用==比较,始终坚持使用===比较。

    • null表示一个“空”的值,undefined表示值未定义大多数情况下,我们都应该用nullundefined仅仅在判断函数参数是否传递的情况下有用。

    • JavaScript的对象是一组由键-值组成的无序集合,例如

      1
      2
      3
      4
      5
      6
      7
      8
      var person = {
      name: 'Bob',
      age: 20,
      tags: ['js', 'web', 'mobile'],
      city: 'Beijing',
      hasCar: true,
      zipcode: null
      };

      要获取一个对象的属性,我们用对象变量.属性名
      的方式:

      1
      2
      person.name; // 'Bob'
      person.zipcode; // null
    • var语句用于声明变量。

      1
      2
      3
      4
      5
      var a;// 申明了变量a,此时a的值为undefined
      var $b = 1;// 申明了变量$b,同时给$b赋值,此时$b的值为1
      var s_007 = '007';// s_007是一个字符串
      var Answer =true;// Answer是一个布尔值true
      var t =null;// t的值是null
    • 'use strict'

      “use strict” 的目的是指定代码在严格条件下执行。

      严格模式下你不能使用未声明的变量。

字符串

  • 转义字符\标识表示'"

  • ASCII字符可以以\x##形式的十六进制表示,例如:

    '\x41'; // 完全等同于 'A'

  • 由于多行字符串用\n写起来比较费事,所以最新的ES6标准新增了一种多行字符串的表示方法,用反引号...表示:

    1
    2
    3
    `这是一个
    多行
    字符串`;

数组

  • indexOf 用于返回指定元素的位置

    1
    2
    3
    4
    5
    var arr = [10, 20, '30', 'xyz'];
    arr.indexOf(10);// 元素10的索引为0
    arr.indexOf(20);// 元素20的索引为1
    arr.indexOf(30);// 元素30没有找到,返回-1
    arr.indexOf('30');// 元素'30'的索引为2
  • slice()就是对应String的substring()版本,它截取Array的部分元素,然后返回一个新的Array

    1
    2
    3
    var arr = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
    arr.slice(0, 3);// 从索引0开始,到索引3结束,但不包括索引3: ['A', 'B', 'C']
    arr.slice(3);// 从索引3开始到结束: ['D', 'E', 'F', 'G']

    如果不给slice()传参,则是复制Array

  • push()Array的末尾添加若干元素,pop()则把Array的最后一个元素删除掉

  • unshift()Array的头部添加若干元素,shift()则把Array的第一个元素删掉

  • sort()可以对当前Array进行排序,它会直接修改当前Array的元素位置,直接调用时,按照默认顺序排序:

    1
    2
    3
    var arr = ['B', 'C', 'A'];
    arr.sort();
    arr;// ['A', 'B', 'C']
  • reverse()把整个Array反转

  • splice()

  • concat()

  • join()

  • 多维数组

对象

JavaScript的对象是一种无序的集合数据类型,它由若干键值对组成。

1
2
3
4
5
6
7
8
var xiaoming = {
name: '小明',
birth: 1990,
school: 'No.1 Middle School',
height: 1.70,
weight: 65,
score: null
};
1
2
3
4
var xiaohong = {
name: '小红',
'middle-school': 'No.1 Middle School'
};

xiaohong的属性名middle-school不是一个有效的变量,就需要用''括起来。访问这个属性也无法使用.操作符,必须用['xxx']来访问:

1
2
3
xiaohong['middle-school']; // 'No.1 Middle School'
xiaohong['name']; // '小红'
xiaohong.name; // '小红'

由于JavaScript的对象是动态类型,你可以自由地给一个对象添加或删除属性:

1
2
3
4
5
6
7
8
var xiaoming = {
name: '小明'
};
xiaoming.age;// undefined
xiaoming.age = 18;// 新增一个age属性
xiaoming.age;// 18delete xiaoming.age;// 删除age属性
xiaoming.age;// undefineddelete xiaoming['name'];// 删除name属性
xiaoming.name;// undefineddelete xiaoming.school;// 删除一个不存在的school属性也不会报错

条件判断、循环、Map and Set与java类似

iterable

  • 为了统一集合类型,ES6标准引入了新的iterable
    类型,ArrayMapSet都属于iterable类型。

  • for ... in循环可以直接循环出Array的索引:

    1
    2
    3
    4
    5
    var a = ['A', 'B', 'C'];
    for (var iin a) {
    console.log(i);// '0', '1', '2'
    console.log(a[i]);// 'A', 'B', 'C'
    }
  • for ... of循环是ES6引入的新的语法,具有iterable
    类型的集合可以通过新的for ... of循环来遍历。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var a = ['A', 'B', 'C'];
    var s =new Set(['A', 'B', 'C']);
    var m =new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
    for (var x of a) {// 遍历Array
    console.log(x);// 'A', 'B', 'C'
    }
    for (var x of s) {// 遍历Set
    console.log(x);// 'A', 'B', 'C'
    }
    for (var x of m) {// 遍历Map
    console.log(x[0] + '=' + x[1]);// 1=x, 2==y 3==z
    }

函数

定义函数——函数也是一个对象

1
2
3
4
5
6
7
function abs(x) {
if (x >= 0) {
return x;
}else {
return -x;
}
}
1
2
3
4
5
6
7
var abs =function (x) {
if (x >= 0) {
return x;
}else {
return -x;
}
};

上述两种定义完全等价,注意第二种方式按照完整语法需要在函数体末尾加一个;,表示赋值语句结束。

1
2
3
4
5
6
XYZ(dest, addr){
offset = caculateOffset(dest, addr)
z_add_routing(offset_z)
y_add_routing(offset_y)
x_add_routing(offset_x)
}

调用函数

1
2
abs(10); // 返回10
abs(-9); // 返回9

参数数量多或少不会影响调用

  • arguments

    JavaScript还有一个免费赠送的关键字arguments,它只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数。

    1
    2
    3
    4
    5
    6
    7
    8
    // foo(a[, b], c)// 接收2~3个参数,b是可选参数,如果只传2个参数,b默认为null:function foo(a, b, c) {
    if (arguments.length === 2) {
    // 实际拿到的参数是a和b,c为undefined
    c = b;// 把b赋给c
    b = null;// b变为默认值
    }
    // ...
    }

方法

函数也可以作为对象的属性,称为对象的方法

1
2
3
4
5
6
7
8
9
10
11
var xiaoming = {
name: '小明',
birth: 1990,
age:function () {
var y =new Date().getFullYear();
return y -this.birth;
}
};

xiaoming.age;// function xiaoming.age()
xiaoming.age();// 今年调用是25,明年调用就变成26了
  • this 始终指向当前对象。

  • apply 可以改变this 指向

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function getAge() {
    var y =new Date().getFullYear();
    return y -this.birth;
    }

    var xiaoming = {
    name: '小明',
    birth: 1990,
    age: getAge
    };

    xiaoming.age();// 25
    getAge.apply(xiaoming, []);// 25, this指向xiaoming, 参数为空
  • 另一个与apply()类似的方法是call(),唯一区别是:

    • apply()把参数打包成Array再传入;
    • call()把参数按顺序传入。

    比如调用Math.max(3, 5, 4),分别用apply()call()实现如下:

    1
    2
    Math.max.apply(null, [3, 5, 4]);// 5
    Math.max.call(null, 3, 5, 4);// 5

高阶函数

JavaScript的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。

1
2
3
function add(x, y, f) {
return f(x) + f(y);
}
  • map/reduce

    • map

      举例说明,比如我们有一个函数f(x)=x2(平方),要把这个函数作用在一个数组[1, 2, 3, 4, 5, 6, 7, 8, 9]上,就可以用map实现如下:

      1
      2
      3
      4
      5
      6
      function pow(x) {
      return x * x;
      }
      var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
      var results = arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
      console.log(results);

      map()作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x2,还可以计算任意复杂的函数,比如,把Array的所有数字转为字符串:

      1
      2
      var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
      arr.map(String);// ['1', '2', '3', '4', '5', '6', '7', '8', '9']
    • reduce

      Array的reduce()把一个函数作用在这个Array[x1, x2, x3...]上,这个函数必须接收两个参数,reduce()把结果继续和序列的下一个元素做累积计算,其效果就是:

      [x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4)

      比方说对一个Array求和,就可以用reduce实现:

      1
      2
      3
      4
      var arr = [1, 3, 5, 7, 9];
      arr.reduce(function (x, y) {
      return x + y;
      });// 25
  • filter

    map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是true还是false决定保留还是丢弃该元素。

    例如,在一个Array中,删掉偶数,只保留奇数,可以这么写:

    1
    2
    3
    4
    5
    var arr = [1, 2, 4, 5, 6, 9, 10, 15];
    var r = arr.filter(function (x) {
    return x % 2 !== 0;
    });
    r;// [1, 5, 9, 15]

    filter()接收的回调函数,其实可以有多个参数。通常我们仅使用第一个参数,表示Array的某个元素。回调函数还可以接收另外两个参数,表示元素的位置和数组本身:

    1
    2
    3
    4
    5
    6
    7
    var arr = ['A', 'B', 'C'];
    var r = arr.filter(function (element, index, self) {
    console.log(element);// 依次打印'A', 'B', 'C'
    console.log(index);// 依次打印0, 1, 2
    console.log(self);// self就是变量arr
    return true;
    });

    element表示arr中的元素,

    index表示arr中的元素索引

    self表示arr变量

  • sort

    Arraysort()方法默认把所有元素先转换为String再排序,结果'10'排在了'2'的前面,因为字符'1'比字符'2'的ASCII码小。

    sort()方法也是一个高阶函数,它还可以接收一个比较函数来实现自定义的排序。

    sort()方法会直接对Array进行修改,它返回的结果仍是当前Array

  • Array

    对于数组,除了map()reducefilter()sort()这些方法可以传入一个函数外,Array对象还提供了很多非常实用的高阶函数。

    • every()方法可以判断数组的所有元素是否满足测试条件。
    • find()方法用于查找符合条件的第一个元素,如果找到了,返回这个元素,否则,返回undefined
    • findIndex()find()类似,也是查找符合条件的第一个元素,不同之处在于findIndex()会返回这个元素的索引,如果没有找到,返回-1
    • forEach()map()类似,它也把每个元素依次作用于传入的函数,但不会返回新的数组forEach()常用于遍历数组,因此,传入的函数不需要返回值

闭包

  • 函数作为返回值

    高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function lazy_sum(arr) {
    var sum =function () {
    return arr.reduce(function (x, y) {
    return x + y;
    });
    }
    return sum;
    }
    var f = lazy_sum([1, 2, 3, 4, 5]); // function sum()
    f(); // 15

    在这个例子中,我们在函数lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。

    闭包是由函数以及声明该函数的词法环境组合而成的

箭头函数

ES6标准新增了一种新的函数:Arrow Function(箭头函数)。

x => x * x

上面的箭头函数相当于:

1
2
3
function (x) {
return x * x;
}

箭头函数相当于匿名函数,并且简化了函数定义。

箭头函数有两种格式,一种像上面的,只包含一个表达式,连{ ... }return都省略掉了。还有一种可以包含多条语句,这时候就不能省略{ ... }return

1
2
3
4
5
6
7
8
x => {
if (x > 0) {
return x * x;
}
else {
return - x * x;
}
}

如果参数不是一个,就需要用括号()括起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 两个参数:
(x, y) => x * x + y * y

// 无参数:
() => 3.14

// 可变参数:
(x, y, ...rest) => {
var i, sum = x + y;
for (i=0; i<rest.length; i++) {
sum += rest[i];
}
return sum;
}

箭头函数看上去是匿名函数的一种简写,但实际上,箭头函数和匿名函数有个明显的区别:箭头函数内部的this是词法作用域,由上下文确定。

回顾前面的例子,由于JavaScript函数对this绑定的错误处理,下面的例子无法得到预期结果:

1
2
3
4
5
6
7
8
9
10
var obj = {
birth: 1990,
getAge:function () {
var b =this.birth;// 1990
var fn =function () {
return new Date().getFullYear() -this.birth;// this指向window或undefined
};
return fn();
}
};

现在,箭头函数完全修复了this的指向,this总是指向词法作用域,也就是外层调用者obj

1
2
3
4
5
6
7
8
var obj = {
birth: 1990,
getAge:function () {
var b =this.birth;// 1990
var fn = () =>new Date().getFullYear() -this.birth;// this指向obj对象return fn();
}
};
obj.getAge();// 25

Generator

generator(生成器)是ES6标准引入的新的数据类型。一个generator看上去像一个函数,但可以返回多次。

generator跟函数很像,定义如下:

1
2
3
4
5
6
function* foo(x) {
yield x + 1;
yield x + 2;
return x + 3;
}

generator和函数不同的是,generator由function*定义(注意多出的*号),并且,除了return语句,还可以用yield返回多次。

斐波那契数列例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
function* fib(max) {
var
t,
a = 0,
b = 1,
n = 0;
while (n < max) {
yield a;
[a, b] = [b, a + b];
n ++;
}
return;
}

直接调用:

1
fib(5); // fib {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}

直接调用一个generator和调用函数不一样,fib(5) 仅仅是创建了一个generator对象,还没有去执行它

调用generator对象有两个方法,一是不断地调用generator对象的next()方法:

1
2
3
4
5
6
7
var f = fib(5);
f.next(); // {value: 0, done:false}
f.next(); // {value: 1, done:false}
f.next(); // {value: 1, done:false}
f.next(); // {value: 2, done:false}
f.next(); // {value: 3, done:false}
f.next(); // {value: undefined, done:true}

第二个方法是直接用for…of 循环迭代generator对象,这种方式不需要我们自己判断done

1
2
3
for (var x of fib(10)) {
console.log(x); // 依次输出0, 1, 1, 2, 3, ...
}

标准对象

Date

1
2
3
4
5
6
7
8
9
10
11
var now = new Date();
now; // Wed Jun 24 2015 19:49:22 GMT+0800 (CST)
now.getFullYear(); // 2015, 年份
now.getMonth(); // 5, 月份,注意月份范围是0~11,5表示六月
now.getDate(); // 24, 表示24号
now.getDay(); // 3, 表示星期三
now.getHours(); // 19, 24小时制
now.getMinutes(); // 49, 分钟
now.getSeconds(); // 22, 秒
now.getMilliseconds(); // 875, 毫秒数
now.getTime(); // 1435146562875, 以number形式表示的时间戳

RegExp

参考github上的learn-regex

https://github.com/ziishaned/learn-regex/blob/master/README.md

JSON

The JSON object contains methods for parsing JavaScript Object Notation (JSON) and converting values to JSON. It can’t be called or constructed, and aside from its two method properties, it has no interesting functionality of its own.

1
2
3
4
5
6
7
8
9
10
11
//JSON->obj
console.log(JSON.parse('{"name":"lmw", "age":21}'))

const replacer = (key, val) => {
if (key === 'x') {
return val * 2
}
return val
}
//obj->JSON
console.log(JSON.stringify({ x: 5, y: 6 }, replacer,'\t'));

需要注意的是:JSON串中key要用双引号括起来

面向对象编程

对于JavaScript来说,所有的数据都是对象,但在JavaScript中的面向对象与C++或者Java却有所不同。

面向对象的两个基本概念:

  1. 类:类型模板,是一个抽象的代表,比如 Student 类表示学生,但是并不具体表示某一个学生。

  2. 实例:实例是根据类创建的对象,比如 Student 类可以创建出 student1student2 等学生,每一个实例都表示一个具体的学生。

在JavaScript中,并没有区分类和实例,而是通过原型(prototype)来实现面向对象编程:

有一个叫robot的学生,用对象表示如下:

1
2
3
4
5
6
7
var robot = {
name: 'Robot',
height: 1.6,
return function(){
console.log(this.name + ' is running...')
}
}

还有一个学生,叫xiaoming,用对象表示和robot几乎一样,只有名字不同,我们可以直接将robot这个对象改为Student,xiaoming再通过改变原型链继承他:

1
2
3
4
5
6
7
8
9
var Student = {
name: 'Robot',
height: 1.6,
return function(){
console.log(this.name + ' is running...')
}
}
xiaoming = Object.create(Student) // 直接通过__proto__改变原型链指向已弃用
xiaoming.name = 'xiaoming'

这样小明也可以执行Student的函数了

1
2
xiaoming.name; // 'xiaoming'
xiaoming.run(); // xiaoming is running...

可以看出在JavaScript中,所有对象都是实例,没有’class‘的概念,继承是通过对象原型指向另一个对象实现的。

创造对象

JavaScript对每个创建的对象都会设置一个原型,指向它的原型对象。

当我们用obj.xxx访问一个对象的属性时,JavaScript引擎先在当前对象上查找该属性,如果没有找到,就到其原型对象上找,如果还没有找到,就一直上溯到Object.prototype对象,最后,如果还没有找到,就只能返回undefined

常用的两种创建对象:

1
2
3
4
var arr = [1,2,3]; // 创建了一个Array对象
var obj = {
name: 'foo' // 创建了一个Object对象
}

构造函数创建对象:

首先先定义一个构造函数:

1
2
3
4
5
6
function Student(name) {
this.name = name;
this.hello = function (){
alert('hello ' + this.name + '!');
}
}

这个构造函数看起来就是普通的函数,只是他在this中写入了一些东西,直接执行的话会在浏览器的window中写入,当我们用new来创建对象的时候,这个函数就编程了一个构造函数,this指向创建的对象

1
2
3
var xiaoming = new Student('xiaoming');
xiaoming.name; // xiaoming
xiaoming.hello(); // hello xiaoming!

原型继承

原型链的关系可以用下图表示:

原型链

概括一下:

构造函数+实例原型实际上就是面向对象中的class,只不过这里拆分开了

  1. 每个实例都有一个构造函数(默认也会创建)
  2. 实例构造函数的 prototype 和实例的 __proto__ 都指向实例原型
  3. 实例原型同时也有一个指向构造函数的 constructor
  4. 实例原型也是一个对象,继承自JavaScript的Object,所以实例原型的 __proto__ 也指向Object.prototype
  5. Oject.prototype也有一个指向构造函数的 constructor和一个从构造函数指向Oject.prototype的 prototype
  6. Oject.prototype的 proto 指向 null

class继承

ES6的class关键字实际上是一种语法糖,可以更方便实现面向对象中的继承。

使用函数实现 Student 类:

1
2
3
4
5
6
7
function Student(name) {
this.name = name;
}

Student.prototype.hello = function() {
alert('hello ' + this.name + '!');
}

改为class

1
2
3
4
5
6
7
8
class Student {
constructor(name) {
this.name = name
}
hello() {
alert('hello ' + this.name + '!');
}
}

继承Student

1
2
3
4
5
6
7
8
9
10
class PrimaryStudent extends Student {
constructor(name, grade) {
super(name) // 调用父类的构造函数
this.grade = grade
}
myGrade() {
alert('I am at grade ' + this.grade);
}
// 可以调用父类的hello
}

后面不完全是JavaScript这门编程语言的内容,就没写了(看了


JavaScript笔记
http://example.com/2022/03/02/JavaScript笔记/
作者
Sonce
发布于
2022年3月2日
许可协议