JavaScript自学手册

JavaScript自学手册

参考链接 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Language_overview

基础知识

变量

let

let 语句声明一个块级作用域的本地变量,并且可选的将其初始化为一个值

const

const 允许声明一个不可变的常量。这个常量在定义域内总是可见的。

var

使用 var 声明的变量在它所声明的整个函数都是可见的。

如果声明了一个变量却没有对其赋值,那么这个变量的类型就是 undefined

运算符

&&|| 运算符使用短路逻辑(short-circuit logic)

如果你用一个字符串加上一个数字(或其他值),那么操作数都会被首先转换为字符串。如下所示:

"3" + 4 + 5; // 345
3 + 4 + "5"; // 75

相等的比较稍微复杂一些。由两个“=(等号)”组成的相等运算符有类型自适应的功能,具体例子如下:

123 == "123"; // true
1 == true; // true

如果在比较前不需要自动类型转换,应该使用由三个“=(等号)”组成的相等运算符:

1 === true; //false
123 === "123"; // false

控制结构

for

常规
for (var i = 0; i < a.length; i++) {
  // Do something with a[i]
}
增强for

ES2015 加入

遍历数组元素

for (let value of array) {
  // do something with value
}

遍历数组索引

for (let property in object) {
  // do something with object property
}
不推荐使用
for (var i in a) {
  // 操作 a[i]
}
forEach

ECMAScript 5 增加了另一个遍历数组的方法,forEach()

["dog", "cat", "hen"].forEach(function (currentValue, index, array) {
  // 操作 currentValue 或者 array[index]
});

switch

switchcase 都可以使用需要运算才能得到结果的表达式;在 switch 的表达式和 case 的表达式是使用 === 严格相等运算符进行比较的:

switch (1 + 3) {
  case 2 + 2:
    yay();
    break;
  default:
    neverhappens();
}

对象

JavaScript 中的对象,Object,可以简单理解成“名称 - 值”对(而不是键值对:现在,ES 2015 的映射表(Map),比对象更接近键值对),不难联想 JavaScript 中的对象与下面这些概念类似:

  • Python 中的字典(Dictionary)
  • Perl 和 Ruby 中的散列/哈希(Hash)
  • C/C++ 中的散列表(Hash table)
  • Java 中的散列映射表(HashMap)
  • PHP 中的关联数组(Associative array)

“名称”部分是一个 JavaScript 字符串,“值”部分可以是任何 JavaScript 的数据类型——包括对象。

创建对象

var obj = new Object();
对象字面量(object literal)
var obj = {};

这两种方法在语义上是相同的。第二种更方便的方法叫作**“对象字面量(object literal)”**法。这种也是 JSON 格式的核心语法,一般我们优先选择第二种方法。

**“对象字面量”**也可以用来在对象实例中定义一个对象:

var obj = {
  name: "Carrot",
  _for: "Max", //'for' 是保留字之一,使用'_for'代替
  details: {
    color: "orange",
    size: 12,
  },
};

对象的属性可以通过链式(chain)表示方法进行访问:

obj.details.color; // orange
obj["details"]["size"]; // 12

下面的例子创建了一个对象原型,Person,和这个原型的实例,You

function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 定义一个对象
var You = new Person("You", 24);
// 我们创建了一个新的 Person,名称是 "You"
// ("You" 是第一个参数,24 是第二个参数..)

完成创建后,对象属性可以通过如下两种方式进行赋值和访问:

点表示法 (dot notation)

// 点表示法 (dot notation)
obj.name = "Simon";
var name = obj.name;

括号表示法 (bracket notation)

// 括号表示法 (bracket notation)
obj["name"] = "Simon";
var name = obj["name"];
// can use a variable to define a key
var user = prompt("what is your key?");
obj[user] = prompt("what is its value?");

这两种方法在语义上也是相同的。

第二种方法的优点在于属性的名称被看作一个字符串,这就意味着它可以在运行时被计算,缺点在于这样的代码有可能无法在后期被解释器优化。

它也可以被用来访问某些以预留关键字作为名称的属性的值:

obj.for = "Simon"; // 语法错误,因为 for 是一个预留关键字
obj["for"] = "Simon"; // 工作正常

数组

创建数组

传统
var a = new Array();
a[0] = "dog";
a[1] = "cat";
a[2] = "hen";
a.length; // 3
数组字面量
var a = ["dog", "cat", "hen"];
a.length; // 3

注意,Array.length 并不总是等于数组中元素的个数,如下所示:

var a = ["dog", "cat", "hen"];
a[100] = "fox";
a.length; // 101

记住:数组的长度是比数组最大索引值多一的数。

如果试图访问一个不存在的数组索引,会得到 undefined

typeof a[90]; // undefined

遍历数组

常规
for (var i = 0; i < a.length; i++) {
  // Do something with a[i]
}
增强for

ES2015 加入

遍历数组元素

for (let value of array) {
  // do something with value
}

遍历数组索引

for (let property in object) {
  // do something with object property
}
不推荐使用
for (var i in a) {
  // 操作 a[i]
}
forEach

ECMAScript 5 增加了另一个遍历数组的方法,forEach()

["dog", "cat", "hen"].forEach(function (currentValue, index, array) {
  // 操作 currentValue 或者 array[index]
});

通用方法

方法名称描述
a.toString()返回一个包含数组中所有元素的字符串,每个元素通过逗号分隔。
a.toLocaleString()根据宿主环境的区域设置,返回一个包含数组中所有元素的字符串,每个元素通过逗号分隔。
a.concat(item1[, item2[, ...[, itemN]]])返回一个数组,这个数组包含原先 aitem1、item2、……、itemN 中的所有元素。
a.join(sep)返回一个包含数组中所有元素的字符串,每个元素通过指定的 sep 分隔。
a.pop()删除并返回数组中的最后一个元素。
a.push(item1, ..., itemN)item1、item2、……、itemN 追加至数组 a
a.reverse()数组逆序(会更改原数组 a)。
a.shift()删除并返回数组中第一个元素。
a.slice(start, end)返回子数组,以 a[start] 开头,以 a[end] 前一个元素结尾。
a.sort([cmpfn])依据可选的比较函数 cmpfn 进行排序,如果未指定比较函数,则按字符顺序比较(即使被比较元素是数字)。
a.splice(start, delcount[, item1[, ...[, itemN]]])start 开始,删除 delcount 个元素,然后插入所有的 item
a.unshift(item1[, item2[, ...[, itemN]]])item 插入数组头部,返回数组新长度(考虑 undefined)。

函数

函数本身就是对象

function add(x, y) {
  var total = x + y;
  return total;
}
  • 一个 JavaScript 函数可以包含 0 个或多个已命名的变量。

  • 函数体中的表达式数量也没有限制。

  • 你可以声明函数自己的局部变量。

  • return 语句在返回一个值并结束函数。

  • 如果没有使用 return 语句,或者一个没有值的 return 语句,JavaScript 会返回 undefined

已命名的参数更像是一个指示而没有其他作用。如果调用函数时没有提供足够的参数,缺少的参数会被 undefined 替代。

add(); // NaN
// 不能在 undefined 对象上进行加法操作

你还可以传入多于函数本身需要参数个数的参数

add(2, 3, 4); // 5
// 将前两个值相加,4 被忽略了

接收任意参数的函数

让我们重写一下上面的函数,使它可以接收任意个数的参数:

function add() {
  var sum = 0;
  for (var i = 0, j = arguments.length; i < j; i++) {
    sum += arguments[i];
  }
  return sum;
}

add(2, 3, 4, 5); // 14

为了使代码变短一些,我们可以使用剩余参数来替换 arguments 的使用。

在这方法中,我们可以传递任意数量的参数到函数中同时尽量减少我们的代码。

这个剩余参数操作符在函数中以:…variable 的形式被使用。

在调用函数时,它将包含所有未被捕获的参数。

function avg(...args) {
  var sum = 0;
  for (let value of args) {
    sum += value;
  }
  return sum / args.length;
}

avg(2, 3, 4, 5); // 3.5

function avg(firstValue, …args) 会把传入函数的第一个值存入 firstValue,其他的参数存入 args

avg() 函数只接受逗号分开的参数列表——但是如果你想要获取一个数组的平均值怎么办?一种方法是将函数按照如下方式重写:

function avgArray(arr) {
  var sum = 0;
  for (var i = 0, j = arr.length; i < j; i++) {
    sum += arr[i];
  }
  return sum / arr.length;
}
avgArray([2, 3, 4, 5]); // 3.5

但如果能重用我们已经创建的那个函数不是更好吗?幸运的是 JavaScript 允许你通过任意函数对象的 apply() 方法来传递给它一个数组作为参数列表。

avg.apply(null, [2, 3, 4, 5]); // 3.5

传给 apply() 的第二个参数是一个数组,它将被当作 avg() 的参数列表使用,至于第一个参数 null,我们将在后面讨论。这也正说明了一个事实——函数也是对象。

备注: 通过使用展开语法,你也可以获得同样的效果。比如说:avg(...numbers)

匿名函数

var avg = function () {
  var sum = 0;
  for (var i = 0, j = arguments.length; i < j; i++) {
    sum += arguments[i];
  }
  return sum / arguments.length;
};

这个函数在语义上与 function avg() 相同。你可以在代码中的任何地方定义这个函数,就像写普通的表达式一样。基于这个特性,有人发明出一些有趣的技巧。与 C 中的块级作用域类似,下面这个例子隐藏了局部变量:

var a = 1;
var b = 2;
(function () {
  var b = 3;
  a += b;
})();

a; // 4
b; // 2

JavaScript 允许以递归方式调用函数。递归在处理树形结构(比如浏览器 DOM)时非常有用。

function countChars(elm) {
  if (elm.nodeType == 3) {
    // 文本节点
    return elm.nodeValue.length;
  }
  var count = 0;
  for (var i = 0, child; (child = elm.childNodes[i]); i++) {
    count += countChars(child);
  }
  return count;
}

这里需要说明一个潜在问题——既然匿名函数没有名字,那该怎么递归调用它呢?在这一点上,JavaScript 允许你命名这个函数表达式。你可以命名立即调用的函数表达式(IIFE——Immediately Invoked Function Expression),如下所示:

var charsInBody = (function counter(elm) {
  if (elm.nodeType == 3) {
    // 文本节点
    return elm.nodeValue.length;
  }
  var count = 0;
  for (var i = 0, child; (child = elm.childNodes[i]); i++) {
    count += counter(child);
  }
  return count;
})(document.body);

如上所提供的函数表达式的名称的作用域仅仅是该函数自身。这允许引擎去做更多的优化,并且这种实现更可读、友好。该名称也显示在调试器和一些堆栈跟踪中,节省了调试时的时间。

需要注意的是 JavaScript 函数是它们本身的对象——就和 JavaScript 其他一切一样——你可以给它们添加属性或者更改它们的属性,这与前面的对象部分一样。

自定义对象

在经典的面向对象语言中,对象是指数据和在这些数据上进行的操作的集合。

与 C++ 和 Java 不同,JavaScript 是一种基于原型的编程语言,并没有 class 语句,而是把函数用作类。

那么让我们来定义一个人名对象,这个对象包括人的姓和名两个域(field)。

名字的表示有两种方法:“名 姓(First Last)”或“姓,名(Last, First)”。

使用我们前面讨论过的函数和对象概念,可以像这样完成定义:

function makePerson(first, last) {
  return {
    first: first,
    last: last,
  };
}
function personFullName(person) {
  return person.first + " " + person.last;
}
function personFullNameReversed(person) {
  return person.last + ", " + person.first;
}

var s = makePerson("Simon", "Willison");
personFullName(s); // "Simon Willison"
personFullNameReversed(s); // "Willison, Simon"

上面的写法虽然可以满足要求,但是看起来很麻烦,因为需要在全局命名空间中写很多函数。

既然函数本身就是对象,如果需要使一个函数隶属于一个对象,那么不难得到:

function makePerson(first, last) {
  return {
    first: first,
    last: last,
    fullName: function () {
      return this.first + " " + this.last;
    },
    fullNameReversed: function () {
      return this.last + ", " + this.first;
    },
  };
}
s = makePerson("Simon", "Willison");
s.fullName(); // "Simon Willison"
s.fullNameReversed(); // Willison, Simon

上面的代码里有一些我们之前没有见过的东西:关键字 this

当使用在函数中时,this 指代当前的对象,也就是调用了函数的对象。

如果在一个对象上使用点或者方括号来访问属性或方法,这个对象就成了 this

如果并没有使用“点”运算符调用某个对象,那么 this 将指向全局对象(global object)。这是一个经常出错的地方。例如:

s = makePerson("Simon", "Willison");
var fullName = s.fullName;
fullName(); // undefined undefined

当我们调用 fullName() 时,this 实际上是指向全局对象的,并没有名为 firstlast 的全局变量,所以它们两个的返回值都会是 undefined

下面使用关键字 this 改进已有的 makePerson函数:

function Person(first, last) {
  this.first = first;
  this.last = last;
  this.fullName = function () {
    return this.first + " " + this.last;
  };
  this.fullNameReversed = function () {
    return this.last + ", " + this.first;
  };
}
var s = new Person("Simon", "Willison");

我们引入了另外一个关键字:new,它和 this 密切相关。

它的作用是创建一个崭新的空对象,然后使用指向那个对象的 this 调用特定的函数。

注意,含有 this 的特定函数不会返回任何值,只会修改 this 对象本身。

new 关键字将生成的 this 对象返回给调用方,而被 new 调用的函数称为构造函数。习惯的做法是将这些函数的首字母大写,这样用 new 调用他们的时候就容易识别了。

不过,这个改进的函数还是和上一个例子一样,在单独调用fullName() 时,会产生相同的问题。

我们的 Person 对象现在已经相当完善了,但还有一些不太好的地方。每次我们创建一个 Person 对象的时候,我们都在其中创建了两个新的函数对象——如果这个代码可以共享不是更好吗?

function personFullName() {
  return this.first + " " + this.last;
}
function personFullNameReversed() {
  return this.last + ", " + this.first;
}
function Person(first, last) {
  this.first = first;
  this.last = last;
  this.fullName = personFullName;
  this.fullNameReversed = personFullNameReversed;
}

这种写法的好处是,我们只需要创建一次方法函数,在构造函数中引用它们。那是否还有更好的方法呢?答案是肯定的。

原型(prototype)

function Person(first, last) {
  this.first = first;
  this.last = last;
}
Person.prototype.fullName = function () {
  return this.first + " " + this.last;
};
Person.prototype.fullNameReversed = function () {
  return this.last + ", " + this.first;
};

Person.prototype 是一个可以被 Person 的所有实例共享的对象。

它是一个名叫原型链(prototype chain)的查询链的一部分:当你试图访问 Person 某个实例(例如上个例子中的 s)一个没有定义的属性时,解释器会首先检查这个 Person.prototype 来判断是否存在这样一个属性。

所以,任何分配给 Person.prototype 的东西对通过 this 对象构造的实例都是可用的。

这个特性功能十分强大,JavaScript 允许你在程序中的任何时候修改原型(prototype)中的一些东西,也就是说你可以在运行时 (runtime) 给已存在的对象添加额外的方法:

s = new Person("Simon", "Willison");
s.firstNameCaps(); // TypeError on line 1: s.firstNameCaps is not a function

Person.prototype.firstNameCaps = function () {
  return this.first.toUpperCase();
};
s.firstNameCaps(); // SIMON

有趣的是,你还可以给 JavaScript 的内置函数原型(prototype)添加东西。让我们给 String 添加一个方法用来返回逆序的字符串:

var s = "Simon";
s.reversed(); // TypeError on line 1: s.reversed is not a function

String.prototype.reversed = function () {
  var r = "";
  for (var i = this.length - 1; i >= 0; i--) {
    r += this[i];
  }
  return r;
};
s.reversed(); // nomiS

正如我前面提到的,原型组成链的一部分。

那条链的根节点是 Object.prototype,它包括 toString() 方法——将对象转换成字符串时调用的方法。这对于调试我们的 Person 对象很有用:

var s = new Person("Simon", "Willison");
s; // [object Object]

Person.prototype.toString = function () {
  return "<Person: " + this.fullName() + ">";
};
s.toString(); // <Person: Simon Willison>

内部函数

javaScript 允许在一个函数内部定义函数,这一点我们在之前的 makePerson() 例子中也见过。关于 JavaScript 中的嵌套函数,一个很重要的细节是,它们可以访问父函数作用域中的变量

function parentFunc() {
  var a = 1;

  function nestedFunc() {
    var b = 4; // parentFunc 无法访问 b
    return a + b;
  }
  return nestedFunc(); // 5
}

闭包

闭包是 JavaScript 中最强大的抽象概念之一——但它也是最容易造成困惑的。它究竟是做什么的呢?

function makeAdder(a) {
  return function (b) {
    return a + b;
  };
}
var add5 = makeAdder(5);
var add20 = makeAdder(20);
add5(6); // ?
add20(7); // ?

makeAdder 这个名字本身,便应该能说明函数是用来做什么的:它会用一个参数来创建一个新的“adder”函数,再用另一个参数来调用被创建的函数时,makeAdder 会将一前一后两个参数相加。

从被创建的函数的视角来看的话,这两个参数的来源问题会更显而易见:新函数自带一个参数——在新函数被创建时,便钦定、钦点了前一个参数(如上方代码中的 a、5 和 20,参考 makeAdder 的结构,它应当位于新函数外部);新函数被调用时,又接收了后一个参数(如上方代码中的 b、6 和 7,位于新函数内部)。最终,新函数被调用的时候,前一个参数便会和由外层函数传入的后一个参数相加。

这里发生的事情和前面介绍过的内嵌函数十分相似:一个函数被定义在了另外一个函数的内部,内部函数可以访问外部函数的变量。唯一的不同是,外部函数已经返回了,那么常识告诉我们局部变量“应该”不再存在。但是它们却仍然存在——否则 adder 函数将不能工作。也就是说,这里存在 makeAdder 的局部变量的两个不同的“副本”——一个是 a 等于 5,另一个是 a 等于 20。那些函数的运行结果就如下所示:

add5(6); // 返回 11
add20(7); // 返回 27

下面来说说,到底发生了什么了不得的事情。每当 JavaScript 执行一个函数时,都会创建一个作用域对象(scope object),用来保存在这个函数中创建的局部变量。它使用一切被传入函数的变量进行初始化(初始化后,它包含一切被传入函数的变量)。这与那些保存的所有全局变量和函数的全局对象(global object)相类似,但仍有一些很重要的区别:第一,每次函数被执行的时候,就会创建一个新的,特定的作用域对象;第二,与全局对象(如浏览器的 window 对象)不同的是,你不能从 JavaScript 代码中直接访问作用域对象,也没有 可以遍历当前作用域对象中的属性 的方法。

所以,当调用 makeAdder 时,解释器创建了一个作用域对象,它带有一个属性:a,这个属性被当作参数传入 makeAdder 函数。然后 makeAdder 返回一个新创建的函数(暂记为 adder)。通常,JavaScript 的垃圾回收器会在这时回收 makeAdder 创建的作用域对象(暂记为 b),但是,makeAdder 的返回值,新函数 adder,拥有一个指向作用域对象 b 的引用。最终,作用域对象 b 不会被垃圾回收器回收,直到没有任何引用指向新函数 adder

作用域对象组成了一个名为作用域链(scope chain)的(调用)链。它和 JavaScript 的对象系统使用的原型(prototype)链相类似。

一个闭包,就是 一个函数 与其 被创建时所带有的作用域对象 的组合。闭包允许你保存状态——所以,它们可以用来代替对象。这个 StackOverflow 帖子里有一些关于闭包的详细介绍。

  • 22
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值