前端学习记录~2023.8.15~JavaScript重难点实例精讲~第7章 ES6(1)
第 7 章 ES6
- 前言
- 7.1 let关键字和const关键字
- 7.1.1 let关键字
- (1)let关键字的特性
- (2)使用let关键字的好处
- 7.1.2 const关键字
- (1)const关键字的特性
- 7.2 解构赋值
- 7.2.1 数组的解构赋值
- (1)数组解构默认值
- (2)交换变量
- (3)解析函数返回的数组
- (4)嵌套数组的解构
- (5)函数参数解构
- 7.2.2 对象的解构赋值
- (1)对象解构的默认值
- (2)嵌套对象的解构
- (3)选择性结构对象的属性
- (4)函数参数解构
- 7.3 扩展运算符与rest运算符
- 7.3.1 扩展运算符
- (1)扩展运算符代替apply()函数
- (2)扩展运算符代替concat()函数合并数组
- (3)扩展运算符转换Set,得到去重的数组
- (4)扩展运算符用于对象拷贝
- 7.3.2 rest运算符
- (1)rest运算符与解构组合使用
- (2)rest运算符代替arguments处理函数参数
- 7.3.3 扩展运算符与rest运算符的关系
- 7.4 模板字符串
- 7.4.1 字符串原生输出
- 7.4.2 字符串变量值传递
- (1)模板字符串中传递基本数据类型的变量
- (2)模板字符串中传递表达式
- (3)模板字符串中传递复杂引用数据类型的变量
- 7.5 箭头函数
- 7.5.1 箭头函数的特点
- (1)语法简洁
- (2)不绑定this
- (3)不支持call()函数与apply()函数的特性
- (4)不绑定arguments
- (5)支持嵌套
- 7.5.2 箭头函数不适用的场景
- (1)不适合作为对象的函数
- (2)不能作为构造函数,不能使用new操作符
- (3)没有prototype属性
- (4)不适合将原型函数定义成箭头函数
- 7.6 ES6对于对象的扩展
- 7.6.1 属性简写
- 7.6.2 属性遍历
- 7.6.3 新增Object.assign()函数
- (1)对象拷贝
- (2)给对象添加属性
- (3)给对象添加函数
- (4)合并对象
- 7.7 Symbol类型
- 7.7.1 Symbol类型的特性
- (1)Symbol值的唯一性
- (2)不能使用new操作符
- (3)不能参与类型运算
- (4)可以使用同一个Symbol值
- 7.7.2 Symbol类型的用法
- (1)用作对象属性名
- (2)用于属性区分
- (3)用于属性名遍历
前言
本章是第七章ES6相关的内容,也是最后一章。本篇为第一部分,后面还会再有一篇。
现在ES6使用非常广泛,新增的箭头函数、类、Promise等新特性,可以方便地处理很多复杂的操作,极大地提高了开发效率。本章会记录ES6中最常用的新特性
在学完后,希望掌握下面知识点:
- let和const关键字
- 解构赋值
- 模板字符串
- 箭头函数
- Symbol类型
- Set和Map数据结构
- Proxy
- Reflect
- Promise
- Iterator
- Generator函数
- Class及其用法
- Module及其用法
7.1 let关键字和const关键字
比起之前只有全局作用域和函数作用域,ES6中新增了块级作用域。
块级作用域表示的是定义的变量可执行上下文环境只能在一个代码块中,一个代码块由一个大括号括住的代码构成,超出这个代码块范围将无法访问内部的变量。
ES6中新增的let关键字和const关键字就是为块级作用域服务的。
7.1.1 let关键字
let关键字用于声明变量,和var关键字的用法类似。但是与var不同的是,let声明的变量只能在let所在的代码块内有效,即在块级作用域内有效,而var声明的变量在块级作用域外仍然有效
{ var a = 1; let b = 2;
}
console.log(a); // 1
console.log(b); // ReferenceError: b is not defined
(1)let关键字的特性
- 在块级作用域内有效
- 不存在变量提升
- 存在暂时性死区:简单点来讲就是,在使用let声明变量之前,该变量都是不可访问的
- 不能重复声明:在同一个作用域内,不能使用let重复声明相同的变量
- 不再是全局对象的属性:在ES6以前,在浏览器环境的全局作用域下,使用var声明的变量、函数表达式或者函数声明均是window对象的属性;在ES6以后,依然遵循上述原则,但是如果是使用let声明的变量或者函数表达式,将不再是window对象的属性
// var声明的变量和函数表达式
var a = 1;
var fn = function () {console.log('global method');
};
console.log(window.a); // 1
window.fn(); // global method// let声明的变量和函数表达式
let b = 2;
let foo = function () { console.log('global method');
};
console.log(window.b); // undefined
window.foo(); // TypeError: window.foo is not a function
(2)使用let关键字的好处
- 不会导致for循环索引值泄露
- 避免出现变量提升导致的变量覆盖问题
- 代替立即执行函数IIFE:立即执行函数(Immediately-Invoked Function Expression,简称IIFE)的内部是一个独立的函数级作用域,使用IIFE的目的主要是避免污染当前作用域内的变量,而使用块级作用域则可以直接避免这个问题
// IIFE写法
(function(){var arg = ...;...
}());// 块级作用域写法
{let arg = ...;...;
}
7.1.2 const关键字
使用const声明的值为一个常量,一旦声明将不会再改变。
如果改变了const声明的常量,则会抛出异常。
const MAX = 123;
MAX = 456; // TypeError: Assignment to constant variable.
使用const声明常量时,在声明时就必须初始化。如果只声明,不初始化,则会抛出异常。
const MAX = 123; // 声明正常
const MIN; // SyntaxError: Missing initializer in const declaration
(1)const关键字的特性
和let关键字拥有一样的特性
- 在块级作用域内有效
- 不存在变量提升
- 存在暂时性死区
- 不能重复声明
- 不再是全局对象的属性
7.2 解构赋值
在日常开发中,我们经常会定义很多的数组或者对象,然后从数组或对象中提取出相关的信息。在传统的ES5及以前的版本中,我们通常会采用下面的方法获取数组或者对象中的值:
var arr = ['one', 'two', 'three'];
var one = arr[0];
var two = arr[1];var obj = {name:'kingx', age:21};
var name = obj.name;
var age = obj.age;
如果数组选项或者对象属性值很多,那么在提取对应值的时候会写很多冗余的代码。
ES6增加了可以简化这种操作的新特性——解构,它可以将值从数组或者对象中提取出来,并赋值到不同的变量中。
主要分两方面:数组的解构赋值和对象的解构赋值
7.2.1 数组的解构赋值
针对数组,在解构赋值时,使用的是模式匹配,只要等号两边数组的模式相同,右边数组的值就会相应赋给左边数组的变量
let [arg1, arg2] = [12, 34];
console.log(arg1); // 12
console.log(arg2); // 34
还可以只解构出感兴趣的值,对不感兴趣的值使用逗号作为占位符,而不指定变量名
let [,,num3] = [12, 34, 56];
console.log(num3); // 56
当右边的数组的值不足以将左边数组的值全部赋值时,会解构失败,对应的值就等于undefined
let [num1, num2, num3] = [12, 34];
console.log(num2); // 34
console.log(num3); // undefined
下面是可以提高效率的一些数组的解构赋值的应用场景:
(1)数组解构默认值
在数组解构时设置默认值,可以防止出现解构得到undefined值的情况
let [num1 = 1, num2] = [, 34];
console.log(num1); // 1
console.log(num2); // 34
需要注意的是,ES6在判断解构是否会得到undefined值时,使用的是严格等于(===)。只有在严格等于undefined的情况下,才会判断该值解构为undefined,相应变量的默认值才会生效。注意下面例子中的num3
let [ num1 = 1, num2 = 2, num3 = 3
] = [null, ''];
console.log(num1); // null
console.log(num2); // ''
console.log(num3); // 3
(2)交换变量
使用数组解构赋值就可以不额外使用临时变量,只需要在等式两边的数组中交换两个变量的顺序即可
var a = 1;
var b = 2;
// 使用数组的解构赋值交换变量
[b, a] = [a, b];
console.log(a); // 2
console.log(b); // 1
(3)解析函数返回的数组
函数返回数组是很常见的场景,在获取数组后我们经常会提取数组中的元素进行后续处理。
使用数组的解构赋值,我们可以快速地获取数组元素值:
function fn() { return [12, 34];
}
let [num1, num2] = fn();
console.log(num1); // 12
console.log(num2); // 34
(4)嵌套数组的解构
在遇到嵌套数组时,即数组中的元素仍然是一个数组,解构的过程会一层层深入,直到左侧数组中的各个变量均已得到确定的值
let [num1, num2, [num3]] = [12, [34, 56], [78, 89]];
console.log(num1); // 12
console.log(num2); // [34, 56]
console.log(num3); // 78
在上面的实例中,num2对应的位置是一个数组,得到的是“[34, 56]”;[num3]得到的是一个数组“[78, 89]”,解构并未完成,对于num3会继续进行解构,最后得到的是数组第一个值“78”
(5)函数参数解构
当函数的参数为数组类型时,可以将实参和形参进行解构
function foo([arg1, arg2]) { console.log(arg1); // 2 console.log(arg2); // 3
}
foo([2, 3]);
7.2.2 对象的解构赋值
数组的解构赋值是基于数组元素的索引,只要左右两侧的数组元素索引相同,便可以进行解构赋值。
但是在对象中,属性是没有顺序的,这就要求右侧解构对象的属性名和左侧定义对象的变量名必须相同,这样才可以进行解构。
同样,未匹配到的变量名在解构时会赋值“undefined”
let {m, n, o} = {m: 'kingx', n: 12};
console.log(m); // kingx
console.log(n); // 12
console.log(o); // undefined
当解构对象的属性名和定义的变量名不同时,必须严格按照key: value的形式补充左侧对象
let {m: name, n: age} = {m: 'kingx', n: 12}; console.log(name); // kingx
console.log(age); // 12
而当key和value值相同时,对于value的省略实际上是一种简写方案
let {m: m, n: n} = {m: 'kingx', n: 12};
// 简写方案
let {m, n} = {m: 'kingx', n: 12};
事实上,对象解构赋值的原理是:先找到左右两侧相同的属性名(key),然后再赋给对应的变量(value),真正被赋值的是value部分,并不是key的部分。
在如下所示的代码中,m作为key,只是用于匹配两侧的属性名是否相同,而真正被赋值的是右侧的name变量,最终name变量会被赋值为“kingx”,而m不会被赋值
let {m: name} = {m: 'kingx'};
console.log(name);// kingx
console.log(m); // ReferenceError: m is not defined
下面是可以提高效率的一些对象的解构赋值的应用场景
(1)对象解构的默认值
对象解构时同样可以设置默认值,默认值生效的条件是对应的属性值严格等于undefined
let {m, n = 1, o = true} = {m: 'kingx', o: null};
console.log(m); // kingx
console.log(n); // 1
console.log(o); // null,因为null与undefined不严格相等,默认值并未生效
当属性名和变量名不相同时,默认值是赋给变量的
let {m, n: age = 1} = {m: 'kingx'};
console.log(m); // kingx
console.log(age); // 1
console.log(n); // ReferenceError: n is not defined
(2)嵌套对象的解构
嵌套的对象同样可以进行解构,解构时从最外层对象向内部逐层进行,每一层对象值都遵循相同的解构规则
let obj = { p: [ 'Hello',{y: 'World'} ]
};
let {p: [x, {y: name}]} = obj;
console.log(x); // Hello
console.log(name); // World
console.log(y); // ReferenceError: y is not defined
注意:当父层对象对应的属性不存在,而解构子层对象时,会出错并抛出异常
let obj = { m: { n: 'kingx'}
};
let {o: {n}} = obj;
console.log(n); //TypeError: Cannot match against 'undefined' or 'null'.
(3)选择性结构对象的属性
假如一个对象有很多通用的函数,在某次处理中,我们只想使用其中的几个函数,那么可以使用解构赋值
let {min, max} = Math;
console.log(min(1,3)); // 1
console.log(max(1,3)); // 3
在上面的实例中,我们只想使用Math对象的min()函数和max()函数,min变量和max变量解构后的值就是Math.min()函数和Math.max()函数,在后面的代码中可以直接使用
(4)函数参数解构
当函数的参数是一个复杂的对象类型时,我们可以通过解构去获得想要获取的值并赋给变量
function whois({displayName: displayName, fullName: {firstName: name}}){ console.log(displayName + "is" + name);
}
const user = { id: 42, displayName: "jdoe", fullName: { firstName: "John", lastName: "Doe"}
};
whois(user); // jdoe is John
在上面的实例中,whois()函数接收的参数是一个复杂的对象类型,可以通过嵌套的对象解构得到我们想要的displayName属性和name属性。
7.3 扩展运算符与rest运算符
在ES6中新增了两种运算符,一种是扩展运算符,另一种是rest运算符。这两种运算符可以很好地解决函数参数和数组元素长度未知情况下的编码问题,使得代码能更加健壮和简洁
7.3.1 扩展运算符
扩展运算符用3个点表示...
,用于将一个数组或类数组对象转换为用逗号分隔的值序列。
它的基本用法是拆解数组和字符串:
const array = [1, 2, 3, 4];
console.log(...array); // 1 2 3 4
const str = "string";
console.log(...str); // s t r i n g
(1)扩展运算符代替apply()函数
扩展运算符可以代替apply()
函数,将数组转换为函数参数
比如想要获取数组最大值时,使用apply()
函数的话:
let arr = [1, 3, 5, 8, 2];
console.log(Math.max.apply(null, arr)); // 8
如果使用扩展运算符:
console.log(Math.max(...arr)); // 8
(2)扩展运算符代替concat()函数合并数组
在ES5中,合并数组时,我们会使用concat()
函数:
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
console.log(arr1.concat(arr2)); // [ 1, 2, 3, 4, 5, 6 ]
如果使用扩展运算符:
console.log([...arr1, ...arr2]); // [ 1, 2, 3, 4, 5, 6 ]
(3)扩展运算符转换Set,得到去重的数组
Set具有自动的去重性质,我们可以再次使用扩展运算符将Set结构转换成数组
let arr = [1, 2, 4, 6, 2, 7, 4];
console.log([...new Set(arr)]); // [1, 2, 4, 6, 7]
(4)扩展运算符用于对象拷贝
let obj = {name: 'kingx'};
var obj2 = {...obj};
使用扩展运算符对数组或对象进行拷贝时,如果数组的元素或者对象的属性是基本数据类型,则支持深拷贝;如果是引用数据类型,则不支持深拷贝。归根结底是因为引用数据类型的拷贝只是复制了引用的地址,拷贝后的对象仍然共享同一个引用地址。
7.3.2 rest运算符
rest运算符同样使用3个点表示...
,其作用与扩展运算符相反,用于将以逗号分隔的值序列转换成数组
(1)rest运算符与解构组合使用
解构会将相同数据结构对应的值赋给对应的变量,但是当我们想将其中的一部分值统一赋给一个变量时,可以使用rest运算符。
let arr = ['one', 'two', 'three', 'four'];
let [arg1, ...arg2] = arr;
console.log(arg1); // one
console.log(arg2); // [ 'two', 'three', 'four' ]
需要注意的是,如果想要使用rest运算符进行解构,则rest运算符对应的变量应该放在最后一位,否则就会抛出异常。因为如果rest运算符不是放在最后一位,变量并不知道要读取多少个数值。
(2)rest运算符代替arguments处理函数参数
在ES6之前,如果我们不确定传入的参数长度,可以统一使用arguments来获取所有传递的参数:
function foo() { for (let arg of arguments) { console.log(arg);}
}
foo('one', 'two', 'three', 'four');// 输出'one', 'two', 'three', 'four'
函数的参数是一个使用逗号分隔的值序列,可以使用rest运算符处理成一个数组,从而确定最终传入的参数,以代替arguments的使用:
function foo(...args) { for (let arg of args) { console.log(arg);}
}
foo('one', 'two', 'three', 'four');// 输出'one', 'two', 'three', 'four'
7.3.3 扩展运算符与rest运算符的关系
通过以上对扩展运算符和rest运算符的讲解,我们知道其实两者是互为逆运算的,扩展运算符是将数组分割成独立的序列,而rest运算符是将独立的序列合并成一个数组。
既然两者都是通过3个点…
来表示的,那么如何去判断这3个点…
属于哪一种运算符呢?我们可以遵循下面的规则:
- 当3个点
…
出现在函数的形参上或者出现在赋值等号的左侧,则表示它为rest运算符 - 当3个点
…
出现在函数的实参上或者出现在赋值等号的右侧,则表示它为扩展运算符
7.4 模板字符串
模板字符串使用反引号``
括起来,它可以当作普通的字符串使用,也可以用来定义多行字符串,同时支持在字符串中使用${}
嵌入变量
7.4.1 字符串原生输出
在传统的字符串输出场景中,我们可能会使用加号+
做拼接,但是拼接出来的字符串会丢失掉代码缩进和换行符:
// 传统字符串方案
var str = 'Hello, my name is kingx, ' + 'I am working in Beijng.';
console.log(str); // Hello, my name is kingx, I am working in Beijng.
在上面的实例中,str变量的第一行字符串和第二行字符串之间使用加号进行拼接,并且字符串中有缩进和换行符,但是输出的结果中它们都被忽略了。
而使用模板字符串语法,会保留字符串内部的空白、缩进和换行符:
let str2 = `Hello, my name is kingx,I am working in Beijng.`;
console.log(str2); // 以下是输出结果
Hello, my name is kingx,I am working in Beijng.
通过模板字符串的语法输出的字符串包含了缩进和换行符。
7.4.2 字符串变量值传递
字符串变量值传递指的是在想要获取的目的字符串中,会包含一些变量。根据变量的不同可以再细分以下几种场景:
(1)模板字符串中传递基本数据类型的变量
如果字符串中包含了变量,在传统的ES5解决方案中,我们会使用加号拼接变量值:
// 传统解决方案
var name = 'kingx';
var address = 'Beijing';
var str = 'Hello, my name is ' + name + ', ' + 'I am working in ' + address + '.';
console.log(str); // Hello, my name is kingx, I am working in Beijng.
如果在一个复杂的语句中,通过变量拼接,会很容易出错,尤其是遇到单引号和双引号同时出现的场景。
而使用模板字符串语法则不会存在上述问题,模板字符串中不再使用加号进行拼接,而是可以直接嵌入变量,只需要将变量写在${}
之中。如果变量未定义,则会抛出异常。
// 模板字符串方案
let name = 'kingx';
let address = 'Beijing';
let str = `Hello, my name is ${name}, I am working in ${address}.`;
console.log(str);
// 以下是输出结果
Hello, my name is kingx,I am working Beijng.
(2)模板字符串中传递表达式
在${}
之中不仅可以传递变量,还可以传递任意的JavaScript表达式,包括数学运算、属性引用、函数调用
// 数学运算
let x = 1, y = 2;
console.log(`${x} + ${y * 2} = ${x + y * 2}`); // 1 + 4 = 5
// 属性引用和数学运算
let obj = {x: 1, y: 2};
console.log(`${obj.x + obj.y}`); // 3
// 函数调用
function fn() { return "Hello World";
}
console.log(`foo ${fn()} bar`); // foo Hello World bar
(3)模板字符串中传递复杂引用数据类型的变量
当传递的变量是一个多层嵌套的复杂引用数据类型值时,模板字符串同样可以支持嵌套解析,遇到表达式会解析成对应的值。
const tmpl = function (addrs) { return ` <table> ${addrs.map(addr => ` <tr><td>${addr.provice}</td></tr> <tr><td>${addr.city}</td></tr>`).join('')} </table> `;
};
const addrs = [{ provice: '湖北省', city: '武汉市'}, {provice: '广东省', city: '广州市'
}];
console.log(tmpl(addrs));
输出的字符串结果如下所示:
</table> <tr><td>湖北省</td></tr> <tr><td>武汉市</td></tr> <tr><td>广东省</td></tr> <tr><td>广州市</td></tr>
</table>
7.5 箭头函数
在ES6中,增加了一种新的函数定义方式——箭头函数=>
。其基本语法如下所示:
// ES6语法
const foo = v => v;
// 等同于传统语法
var foo = function (v){return v;
}
最直观的表现是在编写上省去了function关键字,函数参数和普通的函数参数一样,函数体会被一个大括号括起来
const fn = (num1, num2) => {return num1 + num2;
};
如果函数的参数只有一个,则可以省略小括号;如果函数体只有一行,则可以省略大括号和return关键字
[1, 2, 3].map(r => r * 2); // [2, 4, 6]
// 等同于
[1, 2, 3].map(function(r){return r * 2;
});
7.5.1 箭头函数的特点
(1)语法简洁
(2)不绑定this
在之前3.6节中关于this的指向问题,得出的结论是this永远指向函数的调用者。但是在箭头函数中,this指向的是定义时所在的对象,而不是使用时所在的对象。
从严格意义上讲,箭头函数中不会创建自己的this,而是会从自己作用域链的上一层继承this。
这里我们通过setTimeout()函数和setInterval()函数来看看普通函数和箭头函数的差别:
function Timer() { this.s1 = 0; this.s2 = 0; // 箭头函数 setInterval(() => this.s1++, 1000); // 普通函数 setInterval(function () { this.s2++;}, 1000);
}
let timer = new Timer();
setTimeout(() => console.log('s1: ', timer.s1), 3100); // 3.1秒后输出s1: 3
setTimeout(() => console.log('s2: ', timer.s2), 3100); // 3.1秒后输出s2: 0
在生成Timer的实例timer后,通过setTimeout()函数在3.1秒后输出timer的s1变量,此时setInterval()函数已经执行了3次,由于this.s1++是处在箭头函数中的,这里的this就指向timer,此时timer.s1值为“3”。
而this.s2++是处在普通函数中的,这里的this指向的是全局对象window,实际上相当于window.s2++,结果是window.s2 = 3,而在最后一行的输出结果中,timer.s2仍然为“0”。
(3)不支持call()函数与apply()函数的特性
通过调用call()
函数与apply()
函数可以改变一个函数的执行主体,即改变被调用函数中this的指向。
但是箭头函数却不能达到这一点,因为箭头函数并没有自己的this,而是继承父作用域中的this。
因此在调用call()函数和apply()函数时,如果被调用函数是一个箭头函数,则不会改变箭头函数中this的指向。
let adder = { base : 1,add : function(a) { var f = v => v + this.base; return f(a);},addThruCall: function(a) { var f = v => v + this.base; var b = {base : 2}; return f.call(b, a);}
};
console.log(adder.add(1)); // 2
console.log(adder.addThruCall(1)); // 2
在上面的实例中,执行adder.add(1)时,add()函数内部通过箭头函数的形式定义了f()函数,f()函数中的this会继承至父作用域,即adder,那么this.base = 1,因此执行adder.add(1)相当于执行1 + 1的操作,结果输出“2”。
执行adder.addThruCall(1)时,addThruCall()函数内部通过箭头函数定义了f()函数,其中的this指向了adder。虽然在返回结果时,通过call()函数调用了f()函数,但是并不会改变f()函数中this的指向,this仍然指向adder,而且会接收参数a,因此执行adder.addThruCall(1)相当于执行1 + 1的操作,结果输出“2”。
因此在使用call()函数和apply()函数调用箭头函数时,需要谨慎。
(4)不绑定arguments
在普通的function()函数中,我们可以通过arguments对象来获取到实际传入的参数值,但是在箭头函数中,我们却无法做到这一点
const fn = () => { console.log(arguments);
};
fn(1, 2); // Uncaught ReferenceError: arguments is not defined
因为无法在箭头函数中使用arguments,同样也就无法使用caller和callee属性。
虽然我们无法通过arguments来获取实参,但是我们可以借助rest运算符...
来达到这个目的:
const fn = (...args) => {console.log(args);
};
fn(1, 2); // [1, 2]
(5)支持嵌套
箭头函数支持嵌套的写法,假如我们需要实现这样一个场景:有一个参数会以管道的形式经过两个函数处理,第一个函数处理完的输出将作为第二个函数的输入,两个函数运算完后输出最后的结果:
1 const pipeline = (...funcs) =>
2 val => funcs.reduce((a, b) => b(a), val);
3 const plus1 = a => a + 1;
4 const mult2 = a => a * 2;
5 const addThenMult = pipeline(plus1, mult2);
6 addThenMult(5); // 12
在上面的实例中,我们先看第5行代码,这里调用了pipeline()函数,并传入plus1和 mult2两个参数,返回的是一个函数,在函数中使用reduce()函数先后调用传入的两个处理函数。
在执行第6行代码时,pipeline()函数中的val为5,在第一次执行reduce()函数时,a为5,b为plus1()函数,实际相当于执行5 + 1 = 6,并返回了计算结果。
在第二次执行reduce()函数时,a为上一次返回的结果6,b为mult2()函数,实际相当于执行6×2 = 12,因此最后输出“12”
7.5.2 箭头函数不适用的场景
(1)不适合作为对象的函数
箭头函数并不会绑定this,如果使用箭头函数定义对象字面量的函数,那么其中的this将会指向外层作用域,并不会指向对象本身,因此箭头函数并不适合作为对象的函数。
(2)不能作为构造函数,不能使用new操作符
构造函数是通过new操作符生成对象实例的,生成实例的过程也是通过构造函数给实例绑定this的过程。而箭头函数没有自己的this,因此不能使用箭头函数作为构造函数,也就不能通过new操作符来调用箭头函数。
// 普通函数
function Person(name) { this.name = name;
}
var p = new Person('kingx'); // 正常 // 箭头函数
let Person = (name) => { this.name = name
};
let p = new Person('kingx'); // Uncaught TypeError: Person is not a constructor
(3)没有prototype属性
因为在箭头函数中是没有this的,也就不存在自己的作用域,因此箭头函数是没有prototype属性的。
let a = () => { return 1;
};
console.log(a.prototype); // undefinedfunction b(){ return 2;
}
console.log(b.prototype); // {constructor: ƒ}
(4)不适合将原型函数定义成箭头函数
在给构造函数添加原型函数时,如果使用箭头函数,其中的this会指向全局作用域 window,而并不会指向构造函数,因此并不会访问到构造函数本身,也就无法访问到实例属性,这就失去了作为原型函数的意义
function Person(name) { this.name = name
}
Person.prototype.sayHello = () => { console.log(this); // window console.log(this.name); // undefined
};
let p1 = new Person('kingx');
p1.sayHello();
在上面的代码中,Person()构造函数增加了一个原型函数sayHello(),因为sayHello()函数是通过箭头函数定义的,所以其中的this会指向全局作用域window,从而无法访问到实例的name属性,输出“undefined”。
7.6 ES6对于对象的扩展
ES6对数据结构中的对象进行了扩展,包括数据结构本身和对象新增的函数。
7.6.1 属性简写
传统的JavaScript中,对象都会采用{key: value}的写法,但是在ES6中,可以直接在对象中写入变量,key相当于变量名,value相当于变量值,并且可以直接省略value,通过key表示一个对象的完整属性:
const name = "kingx";
const age = 12;
const obj = {name, age}; //{ name: 'kingx', age: 12 }// 相当于
const obj = { name: name, age: age
};
除了属性可以简写,函数也可以简写,即省略掉关键字function:
const obj = {method: function(){return "hello";}
};
// 等同于
const obj = {method() {return "hello";}
}
7.6.2 属性遍历
到ES6为止,一共有5种方法可以实现对象属性的遍历,具体方法如下所示
for...in
:用于遍历对象自身和继承的可枚举属性(不包含Symbol属性)Object.keys(obj)
:返回一个数组,包含对象自身所有可枚举属性,不包含继承属性和Symbol属性Object.getOwnPropertyNames(obj)
:返回一个数组,包含对象自身所有可枚举属性和不可枚举属性,不包含继承属性和Symbol属性Object.getOwnPropertySymbols(obj)
:返回一个数组,包含对象自身所有Symbol属性,不包含其他属性Reflect.ownKeys(obj)
:返回一个数组,包含自身的可枚举属性、不可枚举属性以及Symbol 属性,不包含继承属性
7.6.3 新增Object.assign()函数
Object.assign()
函数用于将一个或者多个对象的可枚举属性赋值给目标对象,然后返回目标对象。当多个源对象具有相同的属性时,后者的属性值会覆盖前面的属性值。
let target = {a: 1}; //目标对象
let source1 = {b: 2}; //源对象1
let source2 = {c: 3}; //源对象2
let source3 = {c: 4}; //源对象3,和source2对象有同名属性c
console.log(Object.assign(target, source1, source2, source3));
// { a: 1, b: 2, c: 4 }
需要注意的是,Object.assign()
函数无法复制对象的不可枚举属性和继承属性,但可以复制可枚举的Symbol属性
下面是Object.assign()
函数的一些用途
(1)对象拷贝
因为Object.assign()
函数可以复制源对象的属性至目标对象中,所以可以实现对象的拷贝。
需要注意的是,使用Object.assign()
函数进行拷贝时,进行的是浅拷贝。如果属性是基本数据类型,则会复制它的值;如果属性是引用数据类型,则会复制它的引用,对源对象属性值进行的修改会影响到目标对象的属性值,两者实际共享同一个对象的引用。
(2)给对象添加属性
// 传统的写法
function Person(name, age, address) { this.name = name; this.age = age; this.address = address;
}
// Object.assign()写法
function Person(name, age, address) { Object.assign(this, {name, age, address});
}
(3)给对象添加函数
当我们采用传统的写法为对象添加公共的函数时,会扩展其prototype属性,使用Object.assign()
函数也可以简化代码编写方式。
// 传统写法
Person.prototype.getName = function () { return this.name;
};
Person.prototype.getAge = function () { return this.age;
};
// Object.assign()写法
Object.assign(Person.prototype, { getName() { return this.name;}, getAge() { return this.age;}
});
(4)合并对象
使用Object.assgin()
函数即可以将多个对象合并到某个对象中,也可以将多个对象合并为一个新对象并返回,只需要将target设置为空对象{}
即可
// 多个对象合并到一个目标对象中
const merge = (target, ...sources) => Object.assign(target, ...sources);
// 多个对象合并为一个新对象并返回
const merge =(...sources) => Object.assign({}, ...sources);
7.7 Symbol类型
在传统的JavaScript中,对象的属性名都是由字符串构成的。这样就会带来一个问题,假如一个对象继承了另一个对象的属性,我们又需要定义新的属性时,很容易造成属性名的冲突。 为了解决这个问题,ES6引入了一种新的基本数据类型Symbol,它表示的是一个独一无二的值。
至此JavaScript中就一共存在6种基本数据类型,分别是Undefined类型、Null类型、Boolean类型、String类型、Number类型、Symbol类型
7.7.1 Symbol类型的特性
(1)Symbol值的唯一性
Symbol类型的功能类似于一种唯一标识性的ID,通过Symbol()函数来创建一个Symbol值
let s = Symbol();
在Symbol()函数中可以传递一个字符串参数,表示对Symbol值的描述,主要是方便对不同Symbol值的区分。
但是需要注意的是,由于Symbol值的唯一性,任何通过Symbol()函数创建的Symbol值都是不相同的,即使传递了相同的字符串。
const a = Symbol();
const b = Symbol();
const c = Symbol('one');
const d = Symbol('one');
console.log(a === b); // false
console.log(c === d); // false
(2)不能使用new操作符
Symbol函数并不是一个构造函数,因此不能通过new操作符创建Symbol值
let s1 = new Symbol(); // TypeError: Symbol is not a constructor
(3)不能参与类型运算
Symbol值可以通过toString()函数显示地转换为字符串,但是本身不能参与其他类型值的运算,例如在对Symbol值进行字符串拼接操作时,会抛出异常
let s4 = Symbol('hello');
s4.toString(); // Symbol(hello)
's4 content is: ' + s4; // TypeError: Cannot convert a Symbol value to a string
(4)可以使用同一个Symbol值
由于通过Symbol()函数创建的每个值都是不同的,因此如果想使用同一个Symbol值时,需要使用Symbol.for()
函数。
Symbol.for()
函数接收一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个Symbol值;否则就新建并返回一个以该字符串为名称的Symbol值。
let s1 = Symbol.for('one');
let s2 = Symbol.for('one');
s1 === s2; // true
要注意,Symbol.for()
函数只有在没搜索到时才会新建,而Symbol()函数每次调用都会新建一个值。
7.7.2 Symbol类型的用法
(1)用作对象属性名
由于每一个Symbol值都是不相等的,它会经常用作对象的属性名,尤其是当一个对象由多个模块组成时,这样能够避免属性名被覆盖的情况:
需要遵循的一个原则就是为对象字面量新增属性时需要使用方括号
[]
// 新增一个Symbol属性
let PROP_NAME = Symbol();// 第一种写法
let obj = {};
obj[PROP_NAME] = "hello";// 第二种写法
let obj = {[PROP_NAME]: "hello";
};// 第三种写法
let obj = {}
Object.defineProperty(obj, PROP_NAME,{value: "hello";
})
需要注意,不可以通过点运算符为对象添加Symbol属性:
const PROP_NAME = Symbol();
const obj = {};
obj.PROP_NAME = 'Hello!';
console.log(obj[PROP_NAME]); // undefined
console.log(obj['PROP_NAME']); // 'Hello!'
在上面的实例中,我们在通过点运算符为obj增加PROP_NAME属性时,这个PROP_NAME实际是一个字符串,并不是一个Symbol变量。因此我们通过中括号输出PROP_NAME变量对应的值时,得到的是“undefined”;而通过中括号输出’PROP_NAME’字符串值时,得到的是字符串’Hello’。
(2)用于属性区分
(3)用于属性名遍历
使用Symbol作为属性名时,不能通过Object.keys()函数或者for…in来枚举,这样我们可以将一些不需要对外操作和访问的属性通过Symbol来定义。
相关文章:
前端学习记录~2023.8.15~JavaScript重难点实例精讲~第7章 ES6(1)
第 7 章 ES6 前言7.1 let关键字和const关键字7.1.1 let关键字(1)let关键字的特性(2)使用let关键字的好处 7.1.2 const关键字(1)const关键字的特性 7.2 解构赋值7.2.1 数组的解构赋值(1ÿ…...
WebSocket详解以及应用
😜作 者:是江迪呀✒️本文关键词:websocket、网络、长连接、前端☀️每日 一言:任何一个你不喜欢而又离不开的地方,任何一种你不喜欢而又无法摆脱的生活,都是监狱! 一、前言 我们在…...
如何评估开源项目的活跃度和可持续性?
🌷🍁 博主猫头虎 带您 Go to New World.✨🍁 🦄 博客首页——猫头虎的博客🎐 🐳《面试题大全专栏》 文章图文并茂🦕生动形象🦖简单易学!欢迎大家来踩踩~🌺 &a…...
远程Linux/ubuntu服务器后台不间断运行py文件/sh脚本
通常我们在生产环境中运行一些项目时需要将程序不间断的运行在服务器上,并且将日志文件打印到某个文件中,直到程序运行结束,下面介绍了在Linux服务器上不间断运行py文件的方式,以及如何保存相应的日志信息。 对于 .py 文件&#x…...
记录一个诡异的bug
将对接oa跳转到会议转写的项目oa/meetingtranslate项目发布到天宫,结果跳转到successPage后报错 这一看就是successPage接口名没对上啊,查了一下代码,没问题啊。 小心起见,我就把successPage的方法请求方式从Post改为Get和POST都…...
Xamarin.Android中的Fragment
目录 1、Activity中使用Fragment2、Fragment与Activity通信3、Fragment与其他的Fragment通信 1、Activity中使用Fragment 一般而言,会在activity中添加一个加载fragment的方法。通过点击菜单的按钮,加载不同的fragment。其样子一般是这样的:…...
portainer初体验
官方文档 安装 docker 这里采用的的是国内汉化的一个镜像,版本号2.16.2。 地址 docker run -d --restartalways --name"portainer" -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock 6053537/portainer-ce体验 访问9000端口。 尝试&#x…...
4G数传方案(合宙cat1模块)
一. 合宙Cat1简介 合宙 Air724 模组推出的低功耗,超小体积,高性能嵌入式 4G Cat1 核心版,标准的 2.54 排针、最小成本的进项 2G、4G Cat4 切换;主要功能如下: 实际测试工作环境为-35℃-75℃; 支持 5-12V 供电或者 3.7…...
ElasticSearch - 海量数据索引拆分的一些思考
文章目录 困难解决方案初始方案及存在的问题segment merge引入预排序 拆分方案设计考量点如何去除冗余数据按什么维度拆分,拆多少个最终的索引拆分模型演进历程整体迁移流程全量迁移流程流量回放比对验证异步转同步多索引联查优化效果 总结与思考参考 困难 索引数据…...
【SA8295P 源码分析】83 - SA8295P HQNX + Android 完整源代码下载方法介绍
【SA8295P 源码分析】83 - SA8295P HQNX + Android 完整源代码下载方法介绍 一、高通官网 Chipcode 下载步骤介绍1.1 高通Chipcode 下载步骤1.2 高通 ReleaseNote 下载方法二、高通 HQX 代码介绍2.1 完整的 HQX 代码结构:sa8295p-hqx-4-2-4-0_hlos_dev_qnx.tar.gz2.2 sa8295p-…...
【设计模式--原型模式(Prototype Pattern)
一、什么是原型模式 原型模式(Prototype Pattern)是一种创建型设计模式,它的主要目的是通过复制现有对象来创建新的对象,而无需显式地使用构造函数或工厂方法。这种模式允许我们创建一个可定制的原型对象,然后通过复制…...
初识 Redis
初识 Redis 1 认识NoSQL1.1 结构化与非结构化1.2 关联和非关联1.3 查询方式1.4. 事务1.5 总结 2 Redis 概述2.1 应用场景2.2 特性 3 Resis 全局命令4 Redis 基本数据类型4.1 String4.1.1 常用命令4.1.2 命令的时间复杂度4.1.3 使用场景 4.2 Hash4.2.1 常用命令4.2.2 命令的时间…...
php灵异事件,啥都没干数据变了?
这篇文章也可以在我的博客查看 搞WordPress,难免跟php打交道 然而这弱类型语言实在坑有点多 这不今儿又踩了个大坑直接时间-1😅 问题 话不多说直接上代码 <?php $items [1,2];foreach ($items as &$item) {/*empty loop*/} print_r($items)…...
【ffmpeg】基于需要使用videocapture的opencv编译配置(C++)
目录 配置简介ffmpeg源码编译方法记录gstreamer命令行安装方法opencv的编译项记录 配置简介 opencv使用videocapture读取视频流时,需要借助底层的ffmpeg库。如果不能正确编译,会报错,现记录正确编译配置方法。 ffmpeg源码编译方法记录 ope…...
Redisson分布式锁 原理源码 分析
# 基于setnx实现的分布式锁存在的问题: # 为了解决上面的问题,可以用Redisson # Redisson入门 # Redisson可重入锁原理 获取锁的Lua脚本: 释放锁的Lua脚本: # 锁重试原理分析 tryLock()底层代码分析 tim…...
Cocos独立游戏开发框架中的事件管理器
引言 本系列是《8年主程手把手打造Cocos独立游戏开发框架》,欢迎大家关注分享收藏订阅。在独立游戏开发中,事件管理器是一个不可或缺的组件。它为开发者提供了一种灵活的方式来处理游戏内部各种状态变化和用户交互,实现模块之间的解耦和通信…...
keepalived+haproxy 搭建高可用高负载高性能rabbitmq集群
一、环境准备 1. 我这里准备了三台centos7 虚拟机 主机名主机地址软件node-01192.168.157.133rabbitmq、erlang、haproxy、keepalivednode-02192.168.157.134rabbitmq、erlang、haproxy、keepalivednode-03192.168.157.135rabbitmq、erlang 2. 关闭三台机器的防火墙 # 关闭…...
网络安全(黑客)零基础自学
网络安全是什么? 网络安全,顾名思义,网络上的信息安全。 随着信息技术的飞速发展和网络边界的逐渐模糊,关键信息基础设施、重要数据和个人隐私都面临新的威胁和风险。 网络安全工程师要做的,就是保护网络上的信息安…...
如何把本地项目上传github
一、在gitHub上创建新项目 【1】点击添加()-->New repository 【2】填写新项目的配置项 Repository name:项目名称 Description :项目的描述 Choose a license:license 【3】点击确定,项目已在githu…...
跳跃游戏【贪心算法】
跳跃游戏 给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。 判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。在这里插入图片…...
vue2+element-ui 实现下拉框滚动加载
一、自定义滚动指令。 VUE.directive( el-select-loadmore: { bind(el, binding) { const SELECTWRAP_DOM el.querySelector(.el-select-dropdown .el-select-dropdown__wrap) SELECTWRAP_DOM.addEventListener(scroll, function () { /*…...
探索AIGC人工智能(Midjourney篇)(二)
文章目录 利用Midjourney进行LOGO设计 用ChatGPT和Midjourney的AI绘画,制作儿童绘本故事 探索Midjourney换脸艺术 添加InsightFaceSwap机器人 Midjourney打造专属动漫头像 ChatGPT Midjourney画一幅水墨画 Midjourney包装设计之美 Midjourney24节气海报插画…...
01-Flask-简介及环境准备
Flask-简介及环境准备 前言简介特点Flask 与 Django 的比较环境准备 前言 本篇来介绍下Python的web框架–Flask。 简介 Flask 是一个轻量级的 Web 框架,使用 Python 语言编写,较其他同类型框架更为灵活、轻便且容易上手,小型团队在短时间内…...
【Git游戏】远程分支
origin/<branch> 远程分支在本地以 origin/<branch>格式存在,他指向上次和远程分支通过时的记录 git checkout origin/<branch> 会出现HEAD分离的情况 与远程通讯 git fetch —— 从远端获取数据(实际上将本地仓库中的远程分支更新…...
Day07-ElementUI
Day02-ElementUI 一 菜单设计 1 静态菜单 a 在components文件夹中新建一个组件Menu.vue <template><div class="menu-wrap"><el-menuclass="el-menu-vertical-demo"background-color="#031627"text-color="#fff"ac…...
【Go 基础篇】Go语言中的defer和recover:优雅处理错误
Go语言以其简洁、高效和强大的特性受到了开发者的热烈欢迎。在错误处理方面,Go语言提供了一种优雅的机制,即通过defer和recover组合来处理恐慌(panic)错误。本文将详细介绍Go语言中的defer和recover机制,探讨其工作原理…...
4.15 TCP Keepalive 和 HTTP Keep-Alive 是一个东西吗?
目录 HTTP 的 Keep-Alive TCP 的 Keepalive 总结: HTTP的Keep-Alive,是应用层(用户态)实现的,称为HTTP长连接; TCP的Keepalive,是由TCP层(内核态)实现的,…...
如何在VSCode中将html文件打开到浏览器
天行健,君子以自强不息;地势坤,君子以厚德载物。 每个人都有惰性,但不断学习是好好生活的根本,共勉! 文章均为学习整理笔记,分享记录为主,如有错误请指正,共同学习进步。…...
2022年03月 C/C++(四级)真题解析#中国电子学会#全国青少年软件编程等级考试
第1题:拦截导弹 某国为了防御敌国的导弹袭击, 发展出一种导弹拦截系统。 但是这种导弹拦截系统有一个缺陷: 虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。 某天, 雷达捕捉到敌国的…...
五公里场地训练笔记(完整版)
由于考研和口罩等原因,停跑了比较长的时间。中长距离就是这样,修为尽失,大概是要从头开始了,不过还是要乐观的面对,CHEER UP! 翻看咕咚软件,以前的PB是21:12,在2017年9月…...
怎么做网站盈利/深圳市seo网络推广哪家好
一、tar命令打包文件: tar在Linux上是常用的打包、压缩、加压缩工具,参数很多,这里仅仅列举常用的压缩与解压缩参数 参数: -c :create 建立压缩档案的参数; -x : 解压缩压缩档案的参数ÿ…...
小程序设计案例/360手机优化大师下载
电子书原价:2元,联系QQ:3042075372,可以获得限时优惠哟。 有些人会问,为什么现在行动?为什么不等等呢?答案很明了,这个世界不会等你。——《荒野生存》 一切美好的事物,…...
咸宁网页设计/seo站长
首先brew安装前面文章上有的 安装nginx brew install nginx #启动nginx sudo nginx #测试配置是否有语法错误 sudo nginx -t 测试成功 如果不加sudo 会报如下错误: 添加开机启动 sudo vim /Library/LaunchDaemons/com.nginx.plist 在 /Library/LaunchDaemons/ 目…...
wordpress文件的完整url地址/seo工具包括
经过前面几篇文章,包已经可以打了,不管是 jar 包还是 war 包都已测试通过,jsp 页面也可以访问了,但页面上的数据都是在配置文件中写死的,不爽 ~ 到目前为止,最重要的配置还没做,那就是连数据库…...
可以做外链的b2b网站/网络销售都是诈骗公司吗
1:opencv下载源码 在下面网址下载linux版本的源码 http://opencv.org/downloads.html Qt环境的安装配置自行完成(见本博文第7部分) 2:源码解压编译 cd opencv-2.1.0mkdir release && cd releasecmake -D CMAKE_BUILD_TYPERELEASE -D CMAKE_INSTALL_PREFIX/usr/local -…...
营销网站设计公司/企业seo顾问公司
怎么查看自己云服务器信息吗 内容精选换一换如果网站直接无法访问,可能是由于安全组没有放行网站或者远程连接工具使用的端口。本节操作以80端口为例介绍排查云服务器端口不通问题的操作步骤。如果实例无法对外提供HTTP服务,可以按以下思路检查Web服务相…...