JS 系列一:var、let、const、解构、展开、new、this、class、函数

引言

JS系列暂定 27 篇,从基础,到原型,到异步,到设计模式,到架构模式等,此为第一篇:是对 var、let、const、解构、展开、函数 的总结。html

let在不少方面与 var 是类似的,可是 let 能够帮助你们避免在 JavaScript 里常见一些问题。const 是对 let 的一个加强,它能阻止对一个变量再次赋值。前端

1、var 声明

一直以来咱们都是经过 var 关键字定义 JavaScript 变量。git

var num = 1;
复制代码

定义了一个名为 num 值为 1 的变量。程序员

咱们也能够在函数内部定义变量:github

function f() {
    var message = "Hello, An!";

    return message;
}
复制代码

而且咱们也能够在其它函数内部访问相同的变量。typescript

function f() {
    var num = 10;
    return function g() {
        var b = num + 1;
        return b;
    }
}

var g = f();
g(); // 11;
复制代码

上面的例子里,g 能够获取到 f 函数里定义的 num 变量。 每当 g 被调用时,它均可以访问到 f 里的 num 变量。 即便当 gf 已经执行完后才被调用,它仍然能够访问及修改 num设计模式

function f() {
    var num = 1;

    num = 2;
    var b = g();
    num = 3;

    return b;

    function g() {
        return num;
    }
}

f(); // 2
复制代码

做用域规则

对于熟悉其它语言的人来讲,var 声明有些奇怪的做用域规则。 看下面的例子:数组

function f(init) {
    if (init) {
        var x = 10;
    }

    return x;
}

f(true);  // 10
f(false); // undefined
复制代码

在这个例子中,变量 x 是定义在 if 语句里面,可是咱们却能够在语句的外面访问它。架构

这是由于 var 声明能够在包含它的函数,模块,命名空间或全局做用域内部任何位置被访问,包含它的代码块对此没有什么影响。 有些人称此为 var 做用域或函数做用域 。 函数参数也使用函数做用域。异步

这些做用域规则可能会引起一些错误。 其中之一就是,屡次声明同一个变量并不会报错:

function sumArr(arrList) {
    var sum = 0;
    for (var i = 0; i < arrList.length; i++) {
        var arr = arrList[i];
        for (var i = 0; i < arr.length; i++) {
            sum += arr[i];
        }
    }

    return sum;
}
复制代码

这里很容易看出一些问题,里层的 for 循环会覆盖变量 i,由于全部 i 都引用相同的函数做用域内的变量。 有经验的开发者们很清楚,这些问题可能在代码审查时漏掉,引起无穷的麻烦。

捕获变量怪异之处

快速的思考一下下面的代码会返回什么:

for (var i = 0; i < 10; i++) {
    setTimeout(function() { console.log(i); }, 100 * i);
}
复制代码

介绍一下,setTimeout会在若干毫秒的延时后执行一个函数(等待其它代码执行完毕)。

好吧,看一下结果:

10
10
10
10
10
10
10
10
10
10
复制代码

不少 JavaScript 程序员对这种行为已经很熟悉了,但若是你很不解,你并非一我的。 大多数人指望输出结果是这样:

0
1
2
3
4
5
6
7
8
9
复制代码

还记得咱们上面提到的捕获变量吗?

咱们传给 setTimeout 的每个函数表达式实际上都引用了相同做用域里的同一个 i

让咱们花点时间思考一下这是为何。 setTimeout 在若干毫秒后执行一个函数,而且是在 for 循环结束后。for 循环结束后,i 的值为 10。 因此当函数被调用的时候,它会打印出 10

一个一般的解决方法是使用当即执行的函数表达式(IIFE)来捕获每次迭代时i的值:

for (var i = 0; i < 10; i++) {
    (function(i) {
        setTimeout(function() { console.log(i); }, 100 * i);
    })(i);
}
复制代码

这种奇怪的形式咱们已经司空见惯了。 参数 i 会覆盖 for 循环里的 i ,可是由于咱们起了一样的名字,因此咱们不用怎么改 for 循环体里的代码。

2、let 声明

如今你已经知道了 var 存在一些问题,这刚好说明了为何用 let 语句来声明变量。 除了名字不一样外, letvar 的写法一致。

let hello = "Hello,An!";
复制代码

主要的区别不在语法上,而是语义,咱们接下来会深刻研究。

块做用域

当用 let 声明一个变量,它使用的是词法做用域或块做用域。 不一样于使用 var 声明的变量那样能够在包含它们的函数外访问,块做用域变量在包含它们的块或 for 循环以外是不能访问的。

function f(input) {
    let a = 100;

    if (input) {
        // a 被正常引用
        let b = a + 1;
        return b;
    }

    return b;
}
复制代码

这里咱们定义了2个变量 aba 的做用域是 f 函数体内,而 b 的做用域是 if 语句块里。

catch 语句里声明的变量也具备一样的做用域规则。

try {
    throw "oh no!";
}
catch (e) {
    console.log("Oh well.");
}

// Error: 'e' doesn't exist here
console.log(e);
复制代码

拥有块级做用域的变量的另外一个特色是,它们不能在被声明以前读或写。 虽然这些变量始终“存在”于它们的做用域里,但在直到声明它的代码以前的区域都属于 暂时性死区。 它只是用来讲明咱们不能在 let 语句以前访问它们:

a++; 
// Uncaught ReferenceError: Cannot access 'a' before initialization
let a;
复制代码

注意一点,咱们仍然能够在一个拥有块做用域变量被声明前获取它。 只是咱们不能在变量声明前去调用那个函数。

function foo() {
    return a;
}

// 不能在'a'被声明前调用'foo'
// 运行时应该抛出错误
foo();
// Uncaught ReferenceError: Cannot access 'a' before initialization

let a;
复制代码

关于暂时性死区的更多信息,查看这里Mozilla Developer Network.

重定义及屏蔽

咱们提过使用 var 声明时,它不在意你声明多少次;你只会获得1个。

function f(x) {
    var x;
    var x;

    if (true) {
        var x;
    }
}
复制代码

在上面的例子里,全部 x 的声明实际上都引用一个相同的 x,而且这是彻底有效的代码。 这常常会成为 bug 的来源。 好的是, let 声明就不会这么宽松了。

let x = 10;
let x = 20; 
// Uncaught SyntaxError: Identifier 'x' has already been declared
复制代码

并非要求两个均是块级做用域的声明才会给出一个错误的警告。

function f(x) {
    let x = 100; 
    // Uncaught SyntaxError: Identifier 'x' has already been declared
}

function g() {
    let x = 100;
    var x = 100; 
    // Uncaught SyntaxError: Identifier 'x' has already been declared
}
复制代码

并非说块级做用域变量不能用函数做用域变量来声明。 而是块级做用域变量须要在明显不一样的块里声明。

function f(condition, x) {
    if (condition) {
        let x = 100;
        return x;
    }

    return x;
}

f(false, 0); // 0
f(true, 0);  // 100
复制代码

在一个嵌套做用域里引入一个新名字的行为称作 屏蔽 。 它是一把双刃剑,它可能会不当心地引入新问题,同时也可能会解决一些错误。 例如,假设咱们如今用 let 重写以前的 sumArr 函数。

function sumArr(arrList) {
    let sum = 0;
    for (let i = 0; i < arrList.length; i++) {
        var arr = arrList[i];
        for (let i = 0; i < arr.length; i++) {
            sum += arr[i];
        }
    }

    return sum;
}
复制代码

此时将获得正确的结果,由于内层循环的 i 能够屏蔽掉外层循环的 i

一般来说应该避免使用屏蔽,由于咱们须要写出清晰的代码。 同时也有些场景适合利用它,你须要好好打算一下。

块级做用域变量的获取

在咱们最初谈及获取用 var 声明的变量时,咱们简略地探究了一下在获取到了变量以后它的行为是怎样的。 直观地讲,每次进入一个做用域时,它建立了一个变量的环境。 就算做用域内代码已经执行完毕,这个环境与其捕获的变量依然存在。

function theCityThatAlwaysSleeps() {
    let getCity;

    if (true) {
        let city = "Seattle";
        getCity = function() {
            return city;
        }
    }

    return getCity();
}
复制代码

由于咱们已经在 city 的环境里获取到了 city ,因此就算 if 语句执行结束后咱们仍然能够访问它。

回想一下前面 setTimeout 的例子,咱们最后须要使用当即执行的函数表达式来获取每次 for 循环迭代里的状态。 实际上,咱们作的是为获取到的变量建立了一个新的变量环境。

let 声明出如今循环体里时拥有彻底不一样的行为。 不只是在循环里引入了一个新的变量环境,而是针对每次迭代都会建立这样一个新做用域。 这就是咱们在使用当即执行的函数表达式时作的事,因此在 setTimeout例子里咱们仅使用 let 声明就能够了。

for (let i = 0; i < 10 ; i++) {
    setTimeout(function() {console.log(i); }, 100 * i);
}
复制代码

会输出与预料一致的结果:

0
1
2
3
4
5
6
7
8
9
复制代码

3、const 声明

const 声明是声明变量的另外一种方式。

const numLivesForCat = 9;
复制代码

它们与 let 声明类似,可是就像它的名字所表达的,它们被赋值后不能再改变。 换句话说,它们拥有与 let 相同的做用域规则,可是不能对它们从新赋值。

这很好理解,它们引用的值是不可变的

const numLivesForCat = 9;
const kitty = {
    name: "Aurora",
    numLives: numLivesForCat,
}

// Error
kitty = {
    name: "Danielle",
    numLives: numLivesForCat
};

// all "okay"
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;
复制代码

除非你使用特殊的方法去避免,实际上 const 变量的内部状态是可修改的。

4、let vs. const

如今咱们有两种做用域类似的声明方式,咱们天然会问到底应该使用哪一个。 与大多数泛泛的问题同样,答案是:依状况而定。

使用最小特权原则,全部变量除了你计划去修改的都应该使用const。 基本原则就是若是一个变量不须要对它写入,那么其它使用这些代码的人也不可以写入它们,而且要思考为何会须要对这些变量从新赋值。 使用 const也可让咱们更容易的推测数据的流动。

跟据你的本身判断,若是合适的话,与团队成员商议一下。

5、解构

解构数组

最简单的解构莫过于数组的解构赋值了:

let input = [1, 2];
let [first, second] = input;
console.log(first); // 1
console.log(second); // 2
复制代码

这建立了2个命名变量 firstsecond。 至关于使用了索引,但更为方便:

first = input[0];
second = input[1];
复制代码

解构做用于已声明的变量会更好:

[first, second] = [second, first];
复制代码

做用于函数参数:

function f([first, second]) {
    console.log(first);
    console.log(second);
}
f(input);
复制代码

你能够在数组里使用 ... 语法建立剩余变量:

let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // 1
console.log(rest); // [ 2, 3, 4 ]
复制代码

固然,因为是JavaScript, 你能够忽略你不关心的尾随元素:

let [first] = [1, 2, 3, 4];
console.log(first); // 1
复制代码

或其它元素:

let [, second, , fourth] = [1, 2, 3, 4];
复制代码

对象解构

你也能够解构对象:

let o = {
    a: "foo",
    b: 12,
    c: "bar"
};
let { a, b } = o;
复制代码

这经过 o.a and o.b 建立了 ab 。 注意,若是你不须要 c 你能够忽略它。

就像数组解构,你能够用没有声明的赋值:

({ a, b } = { a: "baz", b: 101 });
复制代码

注意,咱们须要用括号将它括起来,由于 Javascript 一般会将以 { 起始的语句解析为一个块。

你能够在对象里使用 ... 语法建立剩余变量:

let { a, ...passthrough } = o;
let total = passthrough.b + passthrough.c.length;
复制代码

6、展开

展开操做符正与解构相反。 它容许你将一个数组展开为另外一个数组,或将一个对象展开为另外一个对象。 例如:

let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];
复制代码

这会令 bothPlus 的值为 [0, 1, 2, 3, 4, 5] 。 展开操做建立了 firstsecond 的一份浅拷贝。 它们不会被展开操做所改变。

你还能够展开对象:

let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { ...defaults, food: "rich" };
复制代码

search 的值为 { food: "rich", price: "$$", ambiance: "noisy" } 。 对象的展开比数组的展开要复杂的多。 像数组展开同样,它是从左至右进行处理,但结果仍为对象。 这就意味着出如今展开对象后面的属性会覆盖前面的属性。 所以,若是咱们修改上面的例子,在结尾处进行展开的话:

let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { food: "rich", ...defaults };
复制代码

那么,defaults 里的 food 属性会重写 food: "rich" ,在这里这并非咱们想要的结果。

对象展开还有其它一些意想不到的限制。 首先,它仅包含对象 自身的可枚举属性。 大致上是说当你展开一个对象实例时,你会丢失其方法:

class C {
  p = 12;
  m() {
  }
}
let c = new C();
let clone = { ...c };
clone.p; // ok
clone.m(); // error!
复制代码

7、new、this、class、函数

this 与 new

new 关键字建立的对象其实是对新对象 this 的不断赋值,并将 __proto__ 指向类的 prototype 所指向的对象

var SuperType = function (name) {
    var nose = 'nose' // 私有属性
    function say () {} // 私有方法
    
    // 特权方法
    this.getName = function () {} 
    this.setName = function () {}
    
    this.mouse = 'mouse' // 对象公有属性
    this.listen = function () {} // 对象公有方法
    
    // 构造器
    this.setName(name)
}

SuperType.age = 10 // 类静态公有属性(对象不能访问)
SuperType.read = function () {} // 类静态公有方法(对象没法访问)

SuperType.prototype = { // 对象赋值(也能够一一赋值)
    isMan: 'true', // 公有属性
    write: function () {} // 公有方法
}

var instance = new SuperType()
复制代码

new

在函数调用前增长 new,至关于把 SuperType 当成一个构造函数(虽然它仅仅只是个函数),而后建立一个 {} 对象并把 SuperType 中的 this 指向那个对象,以即可以经过相似 this.mouse 的形式去设置一些东西,而后把这个对象返回。

具体来说,只要在函数调用前加上 new 操做符,你就能够把任何函数当作一个类的构造函数来用。

加 new

在上例中,咱们能够看到:在构造函数内定义的 私有变量或方法 ,以及类定义的 静态公有属性及方法 ,在 new 的实例对象中都将 没法访问

不加 new

若是你调用 SuperType() 时没有加 new,其中的 this 会指向某个全局且无用的东西(好比,window 或者 undefined),所以咱们的代码会崩溃,或者作一些像设置 window.mouse 之类的傻事。

let instance1 = SuperType(); 

console.log(instance1.mouse); 
// Uncaught TypeError: Cannot read property 'mouse' of undefined

console.log(window.mouse); 
// mouse
复制代码

函数、类

函数
function Bottle(name) {
  this.name = name;
}

// + new
let bottle = new Bottle('bottle'); // ✅ 有效: Bottle {name: "bottle"}
console.log(bottle.name) // bottle

// 不加 new
let bottle1 = Bottle('bottle');   // 🔴 这种调用方法让人很难理解
console.log(bottle1.name); // Uncaught TypeError: Cannot read property 'name' of undefined
console.log(window.name); // bottle
复制代码
class Bottle {
  constructor(name) {
    this.name = name;
  }
  sayHello() {
    console.log('Hello, ' + this.name);
  }
}

// + new
let bottle = new Bottle('bottle');
bottle.sayHello(); // ✅ 依然有效,打印:Hello, bottle

// 不加 new
let bottle1 = Bottle('bottle'); // 🔴 当即失败
// Uncaught TypeError: Class constructor Bottle cannot be invoked without 'new'
复制代码
对比使用
let fun = new Fun();
// ✅ 若是 Fun 是个函数:有效
// ✅ 若是 Fun 是个类:依然有效

let fun1 = Fun(); // 咱们忘记使用 `new`
// 😳 若是 Fun 是个长得像构造函数的方法:使人困惑的行为
// 🔴 若是 Fun 是个类:当即失败
复制代码

new Fun() Fun
class ✅ this 是一个 Fun 实例 🔴 TypeError
function ✅ this 是一个 Fun 实例 😳 this 是 window 或 undefined

使用 new 的怪异之处

return 无效
function Bottle() {
  return 'Hello, AnGe';
}

Bottle(); // ✅ 'Hello, AnGe'
new Bottle(); // 😳 Bottle {}
复制代码
箭头函数

对于箭头函数,使用 new 会报错🔴

const Bottle = () => {console.log('Hello, AnGe')};
new Bottle(); // Uncaught TypeError: Bottle is not a constructor
复制代码

这个行为是遵循箭头函数的设计而刻意为之的。箭头函数的一个附带做用是它没有本身的 this 值 —— this 解析自离得最近的常规函数:

function AnGe() {
    this.name = 'AnGe'
    return () => {console.log('Hello, ' + this.name)};
}
let anGe = new AnGe();
console.log(anGe()); // Hello, AnGe
复制代码

因此**箭头函数没有本身的 this。**但这意味着它做为构造函数是彻底无用的!

总结:箭头函数

  • this 指向定义时的环境。
  • 不可 new 实例化
  • this 不可变。
  • 没有 arguments 对象
容许一个使用 new 调用的函数返回另外一个对象以 覆盖 new 的返回值

先看一个例子:

function Vector(x, y) {
  this.x = x;
  this.y = y;
}

var v1 = new Vector(0, 0);
var v2 = new Vector(0, 0); 

console.log(v1 === v2); // false
v1.x = 1;
console.log(v2); // Vector {x: 0, y: 0}
复制代码

对于这个例子,一目了然,没什么可说的。

那么再看下面一个例子,思考一下为何 b === ctrue 喃😲:

let zeroVector = null;
// 建立了一个懒变量 zeroVector = null;
function Vector(x, y) {
  if (zeroVector !== null) {
    // 复用同一个实例
    return zeroVector;
  }
  zeroVector = this;
  this.x = x;
  this.y = y;
}

var v1 = new Vector(0, 0);
var v2 = new Vector(0, 0); 

console.log(v1 === v2); // true
v1.x = 1;
console.log(v2); // Vector {x: 1, y: 0}
复制代码

这是由于,JavaScript 容许一个使用 new 调用的函数返回另外一个对象以 覆盖 new 的返回值。这在咱们利用诸如「对象池模式」来对组件进行复用时多是有用的。

参考:

TypeScript Variable Declarations

系列文章

想看更过系列文章,点击前往 github 博客主页

走在最后,欢迎关注:前端瓶子君,每日更新

前端瓶子君
相关文章
相关标签/搜索