ES6快速入门(一)函数与作用域

ES6快速入门

一、块级绑定

1.var声明与变量提升

使用var声明的变量,不论在何处都会被视为(声明)在函数级作用域顶部的位置发生。

function getValue(condition) {
    //value: undefined
    console.log(value);
    if (condition) {
        var value = 'blue';
        return value;
    } else {
        //value可以被访问到,其值为undefined
        console.log(value);
        //报错'tang is not defined'
        //console.log(tang);
        return null;
    }
}

getValue(false);

因此可视为:

function getValue(condition) {
    var value;
    console.log(value);
    if (condition) {
        value = 'blue';
        return value;
    } else {
        return null;
    }
}
getValue(false);

变量声明被提升到顶部,但初始化位置没有变。

2.let声明

1)let声明会将变量的作用域限制在当前的代码块中。

function getValue(condition) {
    //报错:value is not defined
    console.log(value);
    if (condition) {
        let value = 'blue';
        return value;
    } else {
        return null;
    }
}
getValue(false);

2)如果一个标识符在当前作用域已经存在,那么再用let声明相同的标识符声明将抛出错误

function test() {
    var count = 99;
    //报错:Identifier 'count' has already been declared
    let count = 100;
}
test();
function test(condition) {
    var count = 99;
    //报错:Identifier 'count' has already been declared
    if (condition) {
        //不会报错
        let count = 100;
    }
}
test();

3.const 声明

使用该关键字声明的变量被视为常量,这意味着它不能被再次赋值(当然必须在声明时初始化)。

其他地方与let声明相同,都是块级声明,不会被提升。

注意:如果const的变量如果是对象,这个对象本身可以被修改。

const person = {
    name: 'Tang'
};
//ok
person.name = 'Jia';
//报错:TypeError: Assignment to constant variable.
person = {
    name: 'Jia'
};

4.暂存性死区(TDZ)

JavaScript 引擎在作用域中寻找变量声明时,会将变量提升到函数/全局作用域的顶部
var) 或是放入 TDZlet const) 内部。任何试图访问 TDZ 内部变量的行为都会以抛出运
行时(runtime) 错误而告终。当执行流达到变量声明的位置时,变量才会被移出 TDZ ,代表
它们可以被安全使用。 

console.log(typeof value);//在TDZ外部:undefined
let condition = true;
if (condition) {
    //报错在TDZ内部
    console.log(typeof value);
    let value = 1;
}

循环中的块级绑定:

for (let i = 0; i < 10; i++) {
    process(items[i]);
} 
// 在这里访问 i 会抛出错误
console.log(i);

循环中的函数问题:

var funcs = [];
for (var i = 0; i < 10; i++) {
    funcs.push(function() { console.log(i); });
}
funcs.forEach(function(func) {
    func(); // 输出 "10" 共10次: 因为循环中创建的函数都保持着对相同变量的引用。
});

使用let解决:

var funcs = [];
for (let i = 0; i < 10; i++) {
    funcs.push(function() {
    console.log(i);
});
}
 funcs.forEach(function(func) {
func(); // 输出 0,1,2 ... 9
})

循环中的const声明:

var funcs = [];
// throws an error after one iteration
for (const i = 0; i < 10; i++) {
    funcs.push(function() {
        console.log(i);
    });
}

5.全局块级绑定

当在全局作用域内使用var声明时会创建一个全局变量,同时也是全局对象的(浏览器环境下的window)的一个属性。

// 在浏览器中运行
var RegExp = "Hello!";
console.log(window.RegExp); // "Hello!" 缺点:很容易就覆盖原了window.RegExp

如果你在全局作用域内使用let和const,那么绑定就会发生在全局作用域内,

但不会向全局作用域对象内部添加任何属性。这意味这你不能使用let或const重写

全局变量仅能屏蔽它们。

// 在浏览器中运行
let RegExp = "Hello!";
console.log(RegExp); // "Hello!"
console.log(window.RegExp === RegExp); // false

二、字符串

1.模板字面量

模板字面量由反引号囊括(在字符串中使用`可以用/转移),模板字面量中无需转移单双引号。

长久存在的BUG,可用来创建多行字符:

var message = "Multiline \
string";
console.log(message); // "Multiline string" 

建议:避免使用BUG达到目的

ES6中我们只需要:

let message = `Multiline
string`;
console.log(message); // "Multiline
// string"
console.log(message.length); // 16

字符置换:允许你将 JavaScript 表达式嵌入到模板字面量中并将其结果作为输出字符串中的一部分

置换部分由 ${ } 包含,其中可以放入任意 JavaScript 表达式

let count = 10,
price = 0.25,
message = `${count} items cost $ ${(count * price).toFixed(2)}.`;
console.log(message); 

标签模板:在一个函数名后面紧跟一个模板字面量,该函数将会调用来处理这个模板字面量。

const a = 5;
const b = 6;
function look() {
    let length = arguments.length;
    for (let i = 0; i < length; i++) {
        console.log(`The arguments[${i}] is ${arguments[i]}`);
    }
}
look`hello ${a + b} world ${a * b}`;
/*
* The arguments[0] is hello , world ,
*The arguments[1] is 11
*The arguments[2] is 30
* */

look函数的第一个参数是一个数组,该数组的成员是模板字符串中那些没有变量替换的部分 

look 函数的其他参数,都是模板字符串各个变量被替换后的值 

三、函数

1.带默认参数的函数

ES5中:

function method(a, b) {
    // a = a || 88; 缺点当a === 0 时会被认为是false.
    // b = b || function () {};

    a = (typeof a !== 'undefined' ? a : 88);
    b = (typeof b !== 'undefined' ? b : function () {});
}

ES6中:

//ES6中url参数是必须传入的,timeout和callback有默认值,是可选的
function mkRequest(url, timeout = 5000, callback = function () {console.log('do sth!')}) {
    console.log(`The timeout is ${timeout}`);
}
mkRequest('/tang');
mkRequest('tang', 8000);
//实际使用的是timeout = 5000
mkRequest('/tang', undefined);
//不使用timeout的默认值
mkRequest('/tang', null);
mkRequest('tang', 6000, function () {console.log('hi')});
/*
* The timeout is 5000
The timeout is 8000
The timeout is 5000
The timeout is null
The timeout is 6000
* */

默认参数对arguments对象的影响

使用默认参数的时候arguments对象的表现是不同的。

ES5非严格模式下,arguments对象会反映出所有被命名参数的变化。

function fake(a, b) {
    console.log(arguments[0] === a); //true
    console.log(arguments[1] === b); //true
    a = 'a fake a';
    b = 'a fake b';
    console.log(arguments[0] === a); //true
    console.log(arguments[1] === b); //true
}
fake('a', 'b');

在ES5严格模式下,arguments不会反应变化:

function fake(a, b) {
    "use strict";
    console.log(arguments[0] === a); //true
    console.log(arguments[1] === b); //true
    a = 'a fake a';
    b = 'a fake b';
    console.log(arguments[0] === a); //false
    console.log(arguments[1] === b); //false
}
fake('a', 'b');

 默认参数的暂存性死区

function getValue(value) {
    return value + 5;
}
function add(first, second
= getValue(first)) { return first + second; }
console.log(add(
1, 1)); // 2 console.log(add(1)); // 7 // 调用 add(1, 1) 的 JavaScript 描述 let first = 1; let second = 1; // 调用 add(1) 的 JavaScript 描述 let first = 1; let second = getValue(first); function add(first = second, second) { return first + second; }
console.log(add(
1, 1)); // 2 console.log(add(undefined, 1)); // 抛出错误 //调用 add(1, 1) 的 JavaScript 描述 let first = 1; let second = 1; // 调用 add(undefined, 1) 的 JavaScript 描述 let first = second; let second = 1;

2. 未命名参数

ES5中的未命名参数,有时会很繁琐。

ES6中的剩余参数:

//keys 是一个囊括所有 object 之后传入参数的剩余参数
//剩余参数的限制:一个函数只能有一个剩余参数,且只能放在最后。
//剩余参数不能用在对象字面量的setter上
function pick(object, ...keys) {
    let result = Object.create(null);
    for (let i = 0, len = keys.length; i < len; i++) {
        result[keys[i]] = object[keys[i]];
    }
    return result;
}

let object = {
// 语法错误:不能在 setter 上使用剩余参数
    set name(...value) {
// do something
    }
};

剩余参数对arguments的影响:arguments总是能够反应传入的所有参数,无视剩余参数的作用。

3. 增强的Function构造函数

var pickFirst = new Function("...args", "return args[0]");

4. 扩展运算符

ES5中如果想在不允许传入数组的函数中传入数组:

let values = [25, 50, 75, 100]
console.log(Math.max.apply(Math, values)); // 100

扩展运算符允许你把一个数组的元素分别作为参数传递给函数。

ES6的扩展运算符使得该需求很容易实现。你可以将数组添加 ... 前缀直接传给
Math.max() 而非调用apply()JavaScript 引擎将数组分解并将其中的元素传给函数,像这
样:

let values = [25, 50, 75, 100]
// 等同于
// console.log(Math.max(25, 50, 75, 100));
console.log(Math.max(...values)); // 100

可以混用扩展运算符和其它参数 :

let values = [-25, -50, -75, -100]
console.log(Math.max(...values, 0)); // 0

5. ES6中的name属性

JavaScript 中多种定义函数的方式使得函数的辨识成为了一种挑战。此外,匿名函数表达式
的流行使得调试更加困难,堆栈在跟踪时难以阅读和解析。由于这个原因,ECMAScript 6
所有的函数添加了 name 属性。

function doSomething() {
}
var doOthers = function () {
};
console.log(doSomething.name); //doSomething
console.log(doOthers.name); //doOthers
var doSomething = function doSomethingElse() {
// ...
};
var person = {
get firstName() {
return "Nicholas"
},
sayName: function() {
console.log(this.name);
}
} c
onsole.log(doSomething.name); // "doSomethingElse"
console.log(person.sayName.name); // "sayName"
console.log(person.firstName.name); // "get firstName"
var doSomething = function() {
// ...
};
console.log(doSomething.bind().name); // "bound doSomething"
console.log((new Function()).name); // "anonymous"

6. 明确函数的双重用途

ECMAScript 5 和早期的版本中,函数的双重用途表现在是否使用 new 来调用它。

当使用new 时,函数中的 this 为一个新的对象并返回它。

function Person(name) {
    this.name = name;
} 
var person = new Person("Nicholas");
var notAPerson = Person("Nicholas");
console.log(person); // "[Object object]"
console.log(notAPerson); // "undefined"

JavaScript 中的函数有两个不同的只有内部(internal-only) 能使用的方法:[[call]]
[[Construct]]。当函数未被 new 调用时,[[call]] 方法会被执行,运行的是函数主体中的代码。
当函数被 new 调用时,[[Construct]] 会被执行并创建了一个新的对象,称为 new target,之
后会执行函数主体并把 this 绑定为该对象。带有 [[Construct]] 方法的函数被称为构造函数
constructor) 。

注意:不是每个函数内部都有 [[Construct]] 方法,所以并非所有的函数都能被 new 调用。

箭头函数小结中提到的箭头函数就没有该方法。

7. ES5中函数调用方式的判断

使用instanceof:

function Person(name) {
    if (this instanceof Person) {
        this.name = name; // 使用 new
    } else {
        throw new Error("你必须使用 new 来调用 Person。")
    }
} 
var person = new Person("Nicholas");
var notAPerson = Person("Nicholas"); // 抛出错误

但不可靠:

var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael"); // 正常运行!

8. 元属性 new.target

为了解决这个问题,ECMAScript 6 引入了 new.target 这个元属性。元属性指的是和目标(如
new) 相关但并非被包含在一个对象内的属性。当函数内部的 [[Construct]] 方法被调用后,
new 操作符调用的目标(target) 将赋给 new.target。该目标通常为创建对象实例并将该实例
赋值给 this 的构造函数。如果 [[call]] 被执行,那么 new.target 的值为 undefined

function Person(name) {
    if (typeof new.target !== "undefined") {
        this.name = name; // 使用 new
    } else {
        throw new Error("你必须使用 new 来调用 Person。")
    }
} 
var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael"); // 错误!

9. 块级函数

ECMAScript 3 或更早的版本中,在块中声明函数(块级函数) 理论上会发生语法错误,但
所有的浏览器却都支持这么做。遗憾的是,每个浏览器支持的方式都有些差异,所以最佳实
践就是不要在块中声明函数(更好的选择是使用函数表达式) 。
为了抑制这种分裂行为,ECMAScript 5 中的严格模式规定在块中声明函数会发生错误: 

"use strict";
if (true) {
// 在 ES5 中抛出错误,ES6不会,块级函数会被提升到块内的顶部
    function doSomething() {
// ...
    }
}

然而 ECMAScript 6 会将 doSomething() 函数视为块级声明并可以在块内的其它部分调用 
块级函数与 let 函数表达式的相似之处是它们都会在执行流跳出定义它们所在的块作用域之后
被销毁。关键的区别是块级函数(声明) 会被提升到顶部,而 let 函数表达式则不会 。

"use strict";
if (true) {
    console.log(typeof doSomething); // 抛出错误
    let doSomething = function () {
// ...
    }
    doSomething();
}
console.log(typeof doSomething);

ECMAScript 6 同样允许非严格模式下块级函数的存在,但是具体行为有些不同。函数的声明
会被提升至函数作用域或全局作用域的顶部,而不是块内 。

// ECMAScript 6 的行为
if (true) {
    console.log(typeof doSomething); // "function"
    function doSomething() {
// ...
    } 
    doSomething();
} 
console.log(typeof doSomething); // "function"

 10. 箭头函数

不同于传统函数的地方:

1)没有this,arguments,super和new.target绑定

2)不能用new调用

3)没有property

4)不能更改 this - this 的值在函数内部不能被修改。在函数的整个生命周期内 this 的值是永恒不变的。 
5)不允许重复的命名参数 - 不论是在严格模式还是非严格模式下,箭头函数都不允许重复的
命名参数存在,相比传统的函数,它们只有在严格模式下才禁止该种行为。

 箭头函数语法:

//传入一个以上参数需要使用括号
var sum = (num1, num2) => num1 + num2;
//没有参数也必须使用括号
var getName = () => "Tang";
//函数体内包含一个以上表达式
var say = (name, age) => {
    var realAge = 1 + age;
    console.log(`My name is ${name} 
     ,and I'm ${realAge}`);
};
//创建空函数必须使用花括号
var doNothing = () => {};

当不想使用传统的函数主体形式返回一个对象字面量的时候,必须将该对象放在括号中。例如:

var getTempItem = id => ({ id: id, name: "Temp" });
// 等同于:
var getTempItem = function(id) {
    return {
        id: id,
        name: "Temp"
    };
};

创建即用函数表达式:

let person = (function (name) {
    return {
        getName: function () {
            return name;
        }
    }
})('Tom');
//ES6:
let person2 = ((name) => ({
    getName: function () {
        return name;
    }
}))('Jimmy');

console.log(person.getName());
console.log(person2.getName());

无this绑定:

JavaScript 编程中最常见的错误之一就是函数中 this 的绑定。由于函数内部的 this 可以在调
用时被上下文替换,所以操作了意想不到的对象的几率很大。

var PageHandler = {
    id: "123456",
    init: function() {
        document.addEventListener("click", function(event) {
            // this指代的是调用该函数的对象,这里是document
            //因此会产生错误
            this.doSomething(event.type);
        }, false);
    },
    //解决方法:
    init2: function() {
        document.addEventListener("click", (function(event) {
            this.doSomething(event.type); // 无错误发生
        }).bind(this), false); //实际上创建了一个带有this绑定的新函数
    },
    doSomething: function(type) {
        console.log("Handling " + type + " for " + this.id);
    }
};

箭头函数没有绑定this,只能通过作用域链来查找:

var PageHandler = {
    id: "123456",
    init: function() {
        document.addEventListener("click",
            event => this.doSomething(event.type), false);
    },
    doSomething: function(type) {
        console.log("Handling " + type + " for " + this.id);
    }
};

箭头函数不能用来定义新类型:

var MyType = () => {},
    object = new MyType(); // 错误 - 你不能使用 new 调用箭头函数

同样,箭头函数中 this 的值由包含它的函数决定,所以你没有办法通过 call()apply()
bind() 方法来改变 this 的值。 

二、扩展的对象功能

1.对象的类别

ES6中规定的对象类别:

普通对象(ordinary object) 拥有 JavaScript 对象所有的默认行为。
特异对象(exotic object) 的某些内部行为和默认的有所差异。
标准对象(standard object) 是 ECMAScript 6 中定义的对象,例如 Array, Date 等,它
们既可能是普通也可能是特异对象。
内置对象(built-in object) 指 JavaScript 执行环境开始运行时已存在的对象。标准对象
均为内置对象。

2.对象字面量语法扩展

简写的属性初始化:

function createPerson(name, age) {
    return {
        name: name,
        age: age
    };
}
//简写
function createPerson(name, age) {
    return {
        name,
        age
    };
}

简写的方法:

var person = {
    name: "Nicholas",
    /*sayName: function() {
        console.log(this.name);
    }*/
    //简写
    sayName() {
        console.log(this.name);
    }
};

动态计算的属性名:

可以在对象字面量中直接使用字符串字面量做属性 

var person = {
    "first name": "Nicholas"
};
console.log(person["first name"]); // "Nicholas"

3.新的方法

ES6在全局Object上添加了几个新的方法:

1)Object.is()

ES6 引入了 Object.is() 方法来补偿严格等于操作符怪异行为的过失。该函数接受两
个参数并在它们相等的返回 true 。只有两者在类型和值都相同的情况下才会判为相等
 

2)Object.assign()

4. 重复的对象字面量属性

"use strict";
var person = {
    name: "Nicholas",
    name: "Greg" // ES5 严格模式下报错 ES6 严格模式下正常运行
};
console.log(person.name); // "Greg"

 5.自身属性的枚举排列

ES5并没有定义枚举对象属性的顺序,并将其交给各 JavaScript 引擎自行决定

 

ES6严格定义了枚举对象自身(own) 属性时返回的属性名顺序 

for-in 循环的枚举顺序仍不明确,因为各 JavaScript 引擎的实现不同。

6.更多的原型属性

1)改变对象的原型:

一般情况下,原型在该对象由构造函数或 Object.create() 方法创建时出现。

ECMAScript 5 下的 JavaScript 编程最重要的约定之一就是一个对象实例无法更改它的原型 

ECMAScript 6 通过添加 Object.setPrototypeOf() 方法来对该约定做了变更。它允许你改变任
何给定对象实例的原型。Object.setPrototypeof() 方法接收两个参数:需要改变原型的对象和
你期望的原型对象 。

let person = {
    greet() {
        console.log('Hello!');
    }
};
let dog = {
    greet() {
        console.log("Wang!");
    }
};

let friend = Object.create(person);
friend.greet(); //Hello!
console.log(Object.getPrototypeOf(friend) === person); //true

Object.setPrototypeOf(friend, dog);
friend.greet(); //Wang!
console.log(Object.getPrototypeOf(friend) === person); //false

2)使用super引用来方便获取property

ES5中:

let friend = {
    getGreeting() {
        return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";
    }
};
// 设定 friend 的原型为 person
Object.setPrototypeOf(friend, person);
console.log(friend.getGreeting()); // "Hello, hi!"
console.log(Object.getPrototypeOf(friend) === person); // true
// 设定 friend 的原型为 dog
Object.setPrototypeOf(friend, dog);
console.log(friend.getGreeting()); // "Woof, hi!"
console.log(Object.getPrototypeOf(friend) === dog); // true

ES6中引入了super:指向当前函数原型的指针,其值等同于Object.getPrototypeOf(this)

let friend = {
    getGreeting() {
// 相比上个例子,等同于:
// Object.getPrototypeOf(this).getGreeting.call(this)
        return super.getGreeting() + ", hi!";
    }
};

你可以使用 super 引用来调用任何存在于原型中的方法。
super 只能在简写方法中使用(concise methods) ,除此之外将发生语法错误 。
另外Object.getPrototypeOf()并不适用于一些场景:

let person = {
    getGreeting() {
        return "Hello";
    }
};
// 原型为 person
let friend = {
    getGreeting() {
        return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";
    }
};
Object.setPrototypeOf(friend, person);
// 原型为 friend
let relative = Object.create(friend);
console.log(person.getGreeting()); // "Hello"
console.log(friend.getGreeting()); // "Hello, hi!"
console.log(relative.getGreeting()); // 错误!

调用 Object.getPrototypeOf() 发生了错误。这是因为 this 的值是 relative,而 relative 的原型
friend 对象。当 this relative 的情况下调用 friend.getGreeting().call() 时,进程反复运作
并持续递归调用该方法,直到抛出栈溢出错误。

可以使用super解决该问题:因为super.getGreeting() 总是指代 person.getGretting()

7.何为"方法"

方法泛指那些对象中值为函数而非数据的属性。ECMAScript 6 正式将方法
定义为带有 [[HomeObject]] 内部属性的函数,该属性指出方法的拥有者 

 

posted @ 2018-10-26 16:10  Shadowplay  阅读(818)  评论(0编辑  收藏  举报