React学习手册读书笔记:JavaScript基础

变量申明

ES2015ES6 )以前,只能通过 var 关键字来声明变量。ES2015 引入了两个新的关键字: constlet

let

我们先来看 let 关键字,它主要是用来解决 var 关键字的作用域提升问题。那么,作用域提升应该怎么理解呢?我们来看一个例子:

1
2
3
4
5
6
7
8
var topic = "JavaScript";

if (topic) {
  var topic = "React";
  console.log("block", topic);    // block React
}

console.log("global", topic);     // global React

这段代码用 var 声明了一个全局变量 topicif 语句体内又声明了同名变量,结果赋值影响到了全局的 topic 。换句话将,if 代码块中的 var 变量声明被提升到全局。

变量的作用域太大,容易引发 BUG 了。为此,ES2015 引入新关键字 let ,支持块级作用域:

1
2
3
4
5
6
7
8
var topic = "JavaScript";

if (topic) {
  let topic = "React";
  console.log("block", topic);    // block React
}

console.log("global", topic);     // global JavaScript

这个例子,if 代码块中的 topic 变量具有块级作用域,只对当前代码块可见。

我们再来看一个 for 循环的例子,var 关键字声明的变量,作用域并不仅限于循环体内:

1
2
3
4
5
6
7
8
var container = document.getElementById("container");
for (var i=0; i<5; i++) {
  var div = document.createElement("div");
  div.onclick = function() {
    alert("This is box #" + i);
  };
  container.appendChild(div);
}

这相当于变量 i 在循环的每次执行都是共享的,循环执行完毕后 i 的值变为 5 。因此,点击回调函数每次都输出 5 。这跟我们的直觉相悖,我们希望每个 div 对应输出 01234

为实现类似的效果,以前我们只能再用一个闭包函数,将 i 包起来:

1
2
3
4
5
6
7
8
var container = document.getElementById("container");
for (var i=0; i<5; i++) {
  var div = document.createElement("div");
  div.onclick = (function(boxno) {
    alert("This is box #" + boxno);
  })(i);
  container.appendChild(div);
}

这种写法不仅繁琐,而且错漏百出,容易引发各种 BUG 。好在 let 支持块级作用域,可以轻松解决这个问题:

1
2
3
4
5
6
7
8
var container = document.getElementById("container");
for (let i=0; i<5; i++) {
  var div = document.createElement("div");
  div.onclick = function() {
    alert("This is box #" + i);
  };
  container.appendChild(div);
}

代码只是将 var 改成 let ,这样变量 i 就是块级作用域,在循环的每次执行中都是独立。

作用域提升意味着经过 var 声明的变量,对全局作用域或者整个函数体(如在函数内声明)可见。我们来看一个例子:

1
2
3
4
5
6
7
8
console.log(`i=${i}`)        // i=undefined

for (var i=0; i<5; i++) {
  // do something
}

console.log(`i=${i}`)        // i=5
console.log(`j=${j}`)        // 抛异常

全局代码中的 for 循环通过 var 声明了变量 i ,对整个全局作用域可见,虽然它是在循环内部声明的。变量甚至在 var 语句之前即可访问,因为它已经声明了,只是未赋值,因此值为 undefined 。相反,如果访问未声明变量,JS 会抛异常。

const

变量在很多场景赋值后就不会改变,有时却被误改,因为引发 BUG 。在 ES2015 后,我们可以通过 const 将变量声明为常量,常量一旦声明就无法修改:

1
2
3
4
// var声明的变量可重新赋值
var disabled = true;
disabled = false;
console.log(disabled); // false
1
2
3
4
// let声明的变量可重新赋值
let disabled = true;
disabled = false;
console.log(disabled); // false
1
2
3
// const声明的变量不可重新赋值
const disabled = true;
disabled = false;      // Uncaught TypeError: Assignment to constant variable.

constlet 一样,也是块级作用域,唯一不同的是变量声明后不可重新赋值。虽然 const 变量无法重新赋值,但我们仍能对变量指向的对象进行修改:

1
2
3
4
5
6
const words = ['apple', 'banana', 'cat'];
words.push('dog');
console.log(words);                       // ['apple', 'banana', 'cat', 'dog']

words = [];
console.log(words);                       // Uncaught TypeError: Assignment to constant variable.

这段代码声明了一个常量 words ,指向一个包含 3 个元素的数组。我们可以直接调用方法直接修改数组,但我们无法将其重新赋值。因此变量是一个引用,保存指向目标对象的指针。const 确保我们无法改变变量指针使其指向另一个对象,但仍可直接修改指针指向的对象。

1
2
3
const count = 10;
count++;
console.log(count); // Uncaught TypeError: Assignment to constant variable.

字符串模板

以前,拼接字符串鬼那么麻烦,要写一堆加好逐个拼接:

1
console.log(lastName + ", " + firstName + " " + middleName);

有了模板字符串后,我们只需写一个字符串,并通过 ${} 语法来插入变量:

1
console.log(`${lastName}, ${firstName} ${middleName}`);

实际上,${} 花括号中可以写任何 JS 表达式:

1
console.log(`1 + 1 = ${1+1}`)

字符串模板支持换行,这在编辑多行文本时非常有用。例如,格式化一份邮件正文:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const email = ` Hello ${firstName},

Thanks for ordering ${qty} tickets to ${event}.

    Order Details
    ${firstName} ${middleName} ${lastName}
         ${qty} x $${price} = $${qty*price} to ${event}

You can pick your tickets up 30 minutes before the show.

Thanks,

${ticketAgent}
`

以前用普通字符串写 HTML ,所有标签都挤在一行,维护起来很费劲。但用模板字符串来组织,可以换行缩进,看起来就清晰多了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
document.body.innerHTML = `
<section>
  <header>
    <h1>The React Blog</h1>
  </header>
  <article>
    <h2>${article.title}</h2>
    ${article.body}
  </article>
  <footer>
    <p>copyright ${new Date().getYear()} | The React Blog</p>
  </footer>
</section>
`;

函数

JS 函数是一等对象,可以被赋值给变量,可以作为参数传给其他函数,也可以作为返回值返回。函数定义有两种不同的语法,一种是 函数声明 ,另一种是 函数表达式(匿名函数)。

函数声明

1
2
3
4
5
function logCompliment() {
  console.log("You're doing great!");
}

logCompliment();

函数表达式

通过函数表达式定义一个匿名函数,并将其赋值给一个变量:

1
2
3
4
5
const logCompliment = function() {
  console.log("You're doing great!");
};

logCompliment();

变量提升

通过函数声明定义的函数,函数名也有作用域提升效果,在定义之前即可调用:

1
2
3
4
5
6
7
// Invoking the function before it's declared
hey();

// Function Declaration
function hey() {
  alert("hey!");
}

意思是函数还不存在就能调用?其实 hey 函数早已存在了,因为 JS 在做语法分析时会识别 function 函数声明,并将其初始化提前。

相反,通过函数表达式定义的函数就没有这种效果:

1
2
3
4
5
6
7
8
9
// Invoking the function before it's declared
hey();

// Function Expression
var hey = function() {
  alert("hey!");
};

// TypeError: hey is not a function

这个例子,变量 key 通过 var 声明,有作用域提升。调用 hey 时,变量仍未赋值,因此其值是 undefined 。由于 undefined 是不可调用的,因此会报错:TypeError: hey is not a function

如果变量是通过 constlet 声明的,就没有作用域提升,调用时会报引用错误(变量 hey 未定义):

1
2
3
4
5
6
7
8
9
// Invoking the function before it's declared
hey();

// Function Expression
const hey = function() {
  alert("hey!");
};

// ReferenceError: hey is not defined

默认参数

包括 C++Python 在内的很多编程语言,都支持给函数参数定义默认值。函数被调用时,如果调用者没给参数传值,就自动取默认值。ES2015 后,JS 也支持这个特性:

1
2
3
4
5
6
function logActivity(name = "Shane McConkey", activity = "swimming") {
  console.log(`${name} loves ${activity}`);
}

logActivity("fasion") // fasion loves swimming
logActivity()         // Shane McConkey loves swimming

第一次调用时,参数 activity 没有传值,因此取默认值 "swimming" 。第二次调用时,两个参数都没有传值,因此都取默认值。

箭头函数

箭头函数也是 ES6 才有的新特性,这种语法不用 function 关键字即可定义函数:

1
2
3
4
5
6
const lordify = (firstName) => {
  return `${firstName} of Canterbury`;
};

console.log(lordify("Dale")); // Dale of Canterbury
console.log(lordify("Gail")); // Gail of Canterbury

这个箭头函数如果通过传统的 function 关键字来定义,应该是这样的:

1
2
3
const lordify = function(firstName) {
  return `${firstName} of Canterbury`;
};

实际上,像这种对参数进行加工并返回新数据的箭头函数,还可以进一步简化:

1
const lordify = firstName => `${firstName} of Canterbury`;

我们直接将返回值写在箭头后即可,不用写函数体和 return 语句。结构大大简化,但效果是一样的。注意到,如果参数只有一个,我们可以省略括号,但参数有多个的话就不能省略。

如果返回对象字面量,直接将对象写在花括号后会报语法错误:

1
2
3
4
5
6
const person = (firstName, lastName) => { // Uncaught SyntaxError: Unexpected token
  first: firstName,
  last: lastName
}

console.log(person("Brad", "Janson"));

箭头后直接跟花括号,会被 JS 当做函数体来处理,所以报语法错误。为了跟函数体语法区分开,我们可以用一个括号将对象字面量括起来:

1
2
3
4
5
6
const person = (firstName, lastName) => ({
  first: firstName,
  last: lastName
});

console.log(person("Flad", "Hanson"));

this指向

普通函数没有绑定 this ,因此 this 会随之环境的变化而改变:

  • 作为函数调用,this 指向 window
  • 作为方法调用,this 指向方法所在的对象;
  • 作为构造函数调用,this 指向被构造的那个新对象;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const whatIsThis = function () {
  console.log(this)
}

const container = {
  whatIsThis,
}

whatIsThis()
container.whatIsThis()

这个例子中,第 9 行为函数调用,this 指向 window ;而第 10 行为方法调用,this 指向 container 这个对象。

而箭头函数就不一样,它在声明时就会绑定 this 。之后不管如何调用,this 都不会变:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const whatIsThis = () => {
  console.log(this)
}

const container = {
  whatIsThis,
}

whatIsThis()
container.whatIsThis()

其实,我们也可以认为箭头函数是没有 this 指针的,而只是可以访问到外层作用域中的 this 而已。如果箭头函数在全局作用域声明,它访问 this 会查找到全局中的 this ,即 window

1
2
3
4
5
6
7
console.log(this);        // window

const showThis = () => {
  console.log(this);      // window
}

showThis();

如果箭头函数全函数调用中声明,它访问 this 会查找函数作用域中的 this

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const getShowThis = function () {
  console.log(this);               // container

  return () => {
    console.log(this);             // container
  };
}

const container = {
  getShowThis,
}

container.getShowThis()();         // container

getShowThis 作为 container 的方法被调用,因此它的 thiscontainer ;而内部声明的箭头函数,则绑定了同一个 this 。无论箭头函数之后如何调用,绑定的 this 不再改变。

由于普通函数 this 指向会随着调用上下文改变,这个特性给程序带来不少陷阱:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const tahoe = {
  mountains: ["Freel", "Rose", "Tallac", "Rubicon", "Silver"],
  print: function(delay = 1000) {
    setTimeout(function() {
      console.log(this.mountains.join(", "));
    }, delay);
  }
};

tahoe.print();
// Uncaught TypeError: Cannot read property 'join' of undefined

这段代码的本意是一秒钟后将 mountains 以逗号拼接后输出,但由于传给 setTimeout 的匿名函数被直接调用,它的 this 指向 window ,而不是 tahoe 对象。与直觉相悖。

我们将传给 setTimeout 的匿名函数改由箭头函数来实现,效果就符合预期了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const tahoe = {
  mountains: ["Freel", "Rose", "Tallac", "Rubicon", "Silver"],
  print: function(delay = 1000) {
    setTimeout(() => {
      console.log(this.mountains.join(", "));
    }, delay);
  }
};

tahoe.print();
// Freel, Rose, Tallac, Rubicon, Silver

print 函数作为 tahoe 对象的方法调用,因此 this 指向 tahoe ;而箭头函数在 print 中声明,因此 this 绑定 print 中的 this ,也是指向 tahoe 对象。

那么,我们是不是应该抛弃普通函数,而只用箭头函数呢?当然不是!如果 pirnt 方法改由箭头函数来实现,同样也有问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const tahoe = {
  mountains: ["Freel", "Rose", "Tallac", "Rubicon", "Silver"],
  print: (delay = 1000) => {
    setTimeout(() => {
      console.log(this.mountains.join(", "));
    }, delay);
  }
};

tahoe.print();
// Uncaught TypeError: Cannot read property 'join' of undefined

这个例子中,print 箭头函数在全局作用域定义,它访问到的 this 就是全局那个 this, 即 window 。因此,选择用普通函数还是箭头函数,还得具体情况具体分析:

  • 需要函数作为对象方法调用,this 指向该对象,则用普通函数;
  • 需要绑定 this ,使其不随调用上下文而改变,则用箭头函数;

代码编译

JS 本来就可以直接在浏览器上执行,不用开发人员自行编译,但这也带来了不少麻烦。

JS 由浏览器负责执行,而用户端浏览器版本有新有旧,对语法特性的支持也是参差不齐。试想,为了兼容旧浏览器,代码不能用新特性,得有多狗血!

好在,我们可以借助 babel 这样的编译工具,对代码进行编译转换。这样,我们可以随心所欲地使用新特性,例如箭头函数和函数默认值:

1
const add = (x = 5, y = 10) => console.log(x + y);

babel 可以帮我们对这行代码进行编译,编译成旧浏览器可以处理的传统语法:

1
2
3
4
5
6
7
"use strict";

var add = function add() {
  var x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 5;
  var y = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 10;
  return console.log(x + y);
};

对象和数组

ES2016 以后,JavaScript 引入了不少新特性,使得对象和数组能够更好地跟变量联动。这些特性在 React 中应用广泛,包括:解构、对象字面量强化以及展开操作符。

对象解构

对象解构赋值语法可以很方便地将对象中的字段赋值给变量:

1
2
3
4
5
6
7
8
9
const sandwich = {
  bread: "dutch crunch",
  meat: "tuna",
  cheese: "swiss",
  toppings: ["lettuce", "tomato", "mustard"]
};

const { bread, meat } = sandwich;
console.log(bread, meat); // dutch crunch tuna

这段代码声明了两个 const 变量 breadmeat ,并分别赋值为对象的同名字段,等价于:

1
2
const bread = sandwich.bread;
const meat = sandwich.meat;

对象结构赋值语法更为简洁清晰,在变量多的情况下更是如此。

数组解构

异步

【小菜学Go语言】系列文章首发于公众号【小菜学编程】,敬请关注:

【小菜笔记】系列文章首发于公众号【小菜学编程】,敬请关注: