1 let 命令的解读
1 let 声明变量,没有变量提升
<script>
// 不存在变量提升
console.log(variable)
let variable = 'zhaoshuai-lc'
</script>
2 作用域
- 全局作用域
- 函数作用域:function() {}
- 块级作用域:{}
let 是一个块作用域
<script>
if (true) {
let variable = 'zhaoshuai-lc'
}
console.log(variable)
</script>
与var定义的变量对比
<script>
if (true) {
var variable = 'zhaoshuai-lc'
}
console.log(variable)
</script>
3 不能重复声明
<script>
let variable = 'zhaoshuai-lc'
let variable = 'zhaoshuai-lc'
</script>
2 const
const 除了 let 的三个特性,还有下面的特性:
1、声明常量,一旦被声明,无法修改
2、可以声明对象,并能修改对象里面的属性值
<script>
const variable = {
name: 'zhaoshuai-lc',
age: 100
}
console.log(variable.name);
variable.name = 'zhaoshuai-la'
console.log(variable.name)
</script>
数组也可以修改元素的值:
<script>
const array = ['zhaoshuai-lc', 'zhaoshuai-la', 'zhaoshuai-lb']
array[1] = 'zhaoyushuai-la'
console.log(array);
</script>
3 作用1、解决 for 变量提升的问题
<script>
var arr = []
for (var i = 0; i < 10; i++) {
arr[i] = function () {
return i;
}
}
var newVar = arr[6]();
console.log(newVar);
</script>
作用2:不会污染全局变量
建议:在默认的情况下用 const,而只有在你知道变量值需要被修改的情况下使用 let。
3 模板字符串
使用 tab 键上面的反引号,插入变量时使用 ${变量名}。
<body>
<div class="box1"></div>
<div class="box2"></div>
<script>
const name = 'zhaoshuai-lc';
const age = 100;
const box_1 = document.querySelector('.box1');
box_1.innerHTML = `<ul>
<li>
${name}
</li>
<li>
${age}
</li>
</ul>`
</script>
</body>
4 函数之默认值、剩余参数
<script>
function add(a, b) {
a = a || 100
b = b || 100
return a + b
}
console.log(add());
</script>
<script>
function add(a, b = 100) {
return a + b;
}
console.log(add(200));
</script>
默认的表达式也可以是一个函数
<script>
function getVal(val) {
return val + 1;
}
function add(a, b = getVal(10)) {
return a + b;
}
console.log(add(10));
</script>
剩余参数
由三个点 … 和一个紧跟着的具名参数指定,比如:…keys,这个解决了 arguments 的问题。
<script>
function getPersonInfo(obj) {
let result = Object.create(null);
console.log(result); // {}
console.log('arguments === ', arguments);
for (let i = 0; i < arguments.length; i++) {
console.log('arguments[i] === ', arguments[i]);
console.log('obj[arguments[i] === ', obj[arguments[i]]);
result[arguments[i]] = obj[arguments[i]];
}
return result;
}
let person = {
name: 'zhaoshuai-lc',
age: 100,
year: 28
}
let personInfo = getPersonInfo(person, 'name', 'age', 'year');
console.log('result === ', personInfo); // {[object Object]: undefined, name: 'zhaoshuai-lc', age: 100, year: 28}
</script>
<script>
function getPersonInfo(obj, ...keys) {
console.log(keys);
let result = Object.create(null);
for (let i = 0; i < keys.length; i++) {
console.log('keys[' + i +'] ===' , keys[i])
console.log('obj[' + keys[i] +'] ===' , obj[keys[i]])
result[keys[i]] = obj[keys[i]]
}
return result;
}
let person = {
name: 'zhaoshuai-lc',
age: 100,
year: 28
}
var personInfo = getPersonInfo(person, 'name', 'age', 'year');
console.log(personInfo);
</script>
剩余参数 对比 arguments:
<script>
function checkArgs(...args) {
console.log(args); // 真实数组
console.log(arguments); // 伪数组
}
checkArgs("name", "author", "year");
</script>
5 函数之扩展运算符、箭头函数
扩展运算符
剩余运算符:把多个独立的参数合并到一个数组中
扩展运算符:将一个数组分割,并将各个项作为分离的参数传递给函数
<script>
const arr = [1, 2, 3, 4, 5, 6, 7, 8]
const maxValue = Math.max(...arr);
console.log(maxValue);
</script>
箭头函数
ES6 允许使用“箭头”(=>)定义函数
<script>
let func = function (val) {
return val;
}
let func_ = val => val;
</script>
如果箭头函数不需要参数或需要多个参数, 就使用一个圆括号代表参数部分.
<script>
let add = (a, b) => a + b;
console.log(add(1, 2));
</script>
由于大括号被解释为代码块, 所以如果箭头函数直接返回一个对象, 必须在对象外面加上括号, 否则会报错
<script>
let getObject = id => ({
id: id,
name: 'zhaoshuai-lc'
})
let obj = getObject(10086);
console.log(obj);
</script>
箭头函数可以与变量解构结合使用
<script>
const full = ({first, last}) => first + last
let person = {
first: 'zhaoshuai-lc',
last: '@inspur.com'
}
console.log(full(person));
</script>
<script>
let mapNew = [1, 2, 3].map(function (val) {
return val * val;
})
console.log(mapNew);
</script>
修改为箭头函数:
<script>
let mapNew = [1, 2, 3].map(val => val *val)
console.log(mapNew);
</script>
使用注意点
箭头函数有几个使用注意点.
(1)函数体内的 [ this ] 对象, 就是定义时所在的对象, 而不是使用时所在的对象.
(2)不可以当作构造函数, 也就是说, 不可以使用new命令, 否则会抛出一个错误.
(3)不可以使用arguments对象, 该对象在函数体内不存在. 如果要用, 可以用 rest 参数代替.
(4)不可以使用yield命令, 因此箭头函数不能用作 Generator 函数
[this]对象的指向是可变的, 但是在箭头函数中, 它是固定的
<script>
function func() {
setTimeout(() => {
console.log('id: ' + this.id)
}, 100)
}
let id = 200
let person = {
name: 'zhaoshuai-lc',
id: 300
}
func.call(person)
</script>
上面代码中, setTimeout()的参数是一个箭头函数,这个箭头函数的定义生效是在func函数生成时,而它的真正执行要等到 100 毫秒后。箭头函数导致 [ this ] 总是指向函数定义生效时所在的对象(本例是person), 所以打印出来的是300.
箭头函数可以让setTimeout里面的 [ this ] , 绑定定义时所在的作用域, 而不是指向运行时所在的作用域
<script>
function Timer() {
this.s1 = 0;
this.s2 = 0;
// 箭头函数
setInterval(() => this.s1++, 1000);
// 普通函数
setInterval(function () {
this.s2++;
}, 1000);
}
var timer = new Timer();
setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0
</script>
Timer函数内部设置了两个定时器 - 分别使用了箭头函数和普通函数:前者的 [ this ] 绑定定义时所在的作用域(即Timer函数), 后者的 [ this ] 指向运行时所在的作用域(即全局对象)。所以,3100 毫秒之后, timer.s1被更新了 3 次,,而timer.s2一次都没更新。
上面代码之中, 只有一个 [ this ] , 就是函数foo的 [ this ] , 所以t1、t2、t3都输出同样的结果. 因为所有的内层函数都是箭头函数, 都没有自己的 [ this ] , 它们的 [ this ] 其实都是最外层foo函数的 [ this ] .
除了 [ this ] , 以下三个变量在箭头函数之中也是不存在的, 指向外层函数的对应变量: arguments、super、new.target.
<script>
function foo() {
setTimeout(() => {
console.log('args:', arguments);
}, 100);
}
foo(2, 4, 6, 8)
// args: [2, 4, 6, 8]
</script>
上面代码中, 箭头函数内部的变量arguments, 其实是函数foo的arguments变量.
另外, 由于箭头函数没有自己的 [ this ] , 所以当然也就不能用call()、apply()、bind()这些方法去改变 [ this ] 的指向.
<script>
(function () {
return [
(() => this.x).bind({x: 'inner'})()
];
}).call({x: 'outer'});
// ['outer']
</script>
上面代码中, 箭头函数没有自己的 [ this ] , 所以bind方法无效, 内部的 [ this ] 指向外部的 [ this ] .
由于箭头函数使得 [ this ] 从“动态”变成“静态”, 下面两个场合不应该使用箭头函数。第一个场合是定义对象的方法, 且该方法内部包括 [ this ]:
上面代码中, cat.jumps()方法是一个箭头函数, 这是错误的. 调用cat.jumps()时, 如果是普通函数, 该方法内部的 [ this ] 指向cat;如果写成上面那样的箭头函数, 使得 [ this ] 指向全局对象, 因此不会得到预期结果. 这是因为对象不构成单独的作用域, 导致jumps箭头函数定义时的作用域就是全局作用域.
第二个场合是需要动态 [ this ] 的时候, 也不应使用箭头函数:
上面代码运行时, 点击按钮会报错, 因为button的监听函数是一个箭头函数, 导致里面的 [ this ] 就是全局对象. 如果改成普通函数, [ this ] 就会动态指向被点击的按钮对象.
另外, 如果函数体很复杂, 有许多行, 或者函数内部有大量的读写操作, 不单纯是为了计算值, 这时也不应该使用箭头函数, 而是要使用普通函数, 这样可以提高代码可读性
6 赋值解构
解构赋值是对赋值运算符的一种拓展,它针对数组和对象来进行操作。
<script>
let obj = {
a: {
name: "kaimo"
},
b: [],
c: "hello cat"
}
// 不完全解构
let {a} = obj;
console.log(a);
</script>
<script>
let obj = {
a: {
name: "kaimo"
},
b: [],
c: "hello cat"
}
// 剩余运算符
let {a, ...res} = obj;
console.log(a)
console.log(res)
</script>
<script>
let {a, b = 30} = {a: 20};
console.log(a);
console.log(b);
</script>
对数组解构:
7 扩展的对象的功能
1 属性的简洁表示
2 CommonJS 模块输出一组变量, 就非常合适使用简洁写法
3 简洁写法在 属性 赋值器 和 取值器 中的应用
8 属性的可枚举性和遍历
for…in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)
<script>
let person = {
name: 'name',
age: 22,
gender: 'male',
hobby: ['打球', '读书', '写字']
}
for (let personKey in person) {
console.log(personKey, person[personKey]);
}
</script>
Object.keys返回一个数组, 包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名
<script>
let person = {
name: 'name',
age: 22,
gender: 'male',
hobby: ['打球', '读书', '写字']
}
let propertyList = Object.keys(person);
for (let property of propertyList) {
console.log(property, person[property]);
}
</script>
Object.getOwnPropertyNames返回一个数组, 包含对象自身的所有属性(不含 Symbol 属性, 但是包括不可枚举属性)的键名.
<script>
let person = {
name: 'name',
age: 22,
gender: 'male',
hobby: ['打球', '读书', '写字']
}
let propertyList = Object.getOwnPropertyNames(person);
for (let property of propertyList) {
console.log(property, person[property]);
}
</script>
Object.getOwnPropertySymbols返回一个数组, 包含对象自身的所有 Symbol 属性的键名.
<script>
let person = {
name: 'name',
age: 22,
gender: 'male',
hobby: ['打球', '读书', '写字']
}
let propertyList = Object.getOwnPropertySymbols(person);
for (let property of propertyList) {
console.log(property, person[property]);
}
</script>
Reflect.ownKeys返回一个数组, 包含对象自身的(不含继承的)所有键名, 不管键名是 Symbol 或字符串, 也不管是否可枚举
<script>
let person = {
name: 'name',
age: 22,
gender: 'male',
hobby: ['打球', '读书', '写字']
}
let propertyList = Reflect.ownKeys(person)
for (let property of propertyList) {
console.log(property, person[property]);
}
</script>
以上的 5 种方法遍历对象的键名, 都遵守同样的属性遍历的次序规则.
- 首先遍历所有数值键, 按照数值升序排列.
- 其次遍历所有字符串键, 按照加入时间升序排列.
- 最后遍历所有 Symbol 键, 按照加入时间升序排列.
9 super 关键字
我们知道, this关键字总是指向函数所在的当前对象 , ES6 又新增了另一个类似的关键字 [ super ], 指向当前对象的原型对象.
<script>
const proto = {foo: 'hello'};
const obj = {
foo: 'world',
find() {
return super.foo;
}
};
console.log('obj-1 ', obj);
Object.setPrototypeOf(obj, proto);
console.log('obj-2 ', obj);
obj.find() // "hello"
</script>
上面代码中, 对象obj.find()方法之中, 通过super.foo引用了原型对象proto的foo属性.
注意, super关键字表示原型对象时, 只能用在对象的方法之中, 用在其他地方都会报错.
<script>
// 报错
const obj = {
foo: super.foo
}
// 报错
const obj = {
foo: () => super.foo
}
// 报错
const obj = {
foo: function () {
return super.foo
}
}
</script>
上面三种super的用法都会报错, 因为对于 JavaScript 引擎来说, 这里的super都没有用在对象的方法之中.
- 第一种写法是super用在属性里面,
- 第二种和第三种写法是super用在一个函数里面, 然后赋值给foo属性.
目前, 只有对象方法的简写法可以让 JavaScript 引擎确认, 定义的是对象的方法.
JavaScript 引擎内部, super.foo等同于Object.getPrototypeOf(this).foo(属性)或Object.getPrototypeOf(this).foo.call(this)(方法).
<script>
const proto = {
x: 'hello',
foo() {
console.log(this.x);
},
};
const obj = {
x: 'world',
foo() {
super.foo();
}
}
Object.setPrototypeOf(obj, proto);
obj.foo() // "world"
</script>
上面代码中, super.foo指向原型对象proto的foo方法, 但是绑定的this却还是当前对象obj, 因此输出的就是world.
10 对象的新增方法
1 Object.is()
ES5 比较两个值是否相等,只有两个运算符: 相等运算符(==)和严格相等运算符(===).
它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0.
JavaScript 缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等.
ES6 提出“Same-value equality”(同值相等)算法,用来解决这个问题. [ Object.is ] 就是部署这个算法的新方法. 它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致.
<script>
let variable = 'hello-zhaoshuai-lc'
let variable_ = 'hello-zhaoshuai-lc'
var res = Object.is(variable, variable_);
console.log(res); // true
</script>
<script>
let variable = 100
let variable_ = '100'
var res = Object.is(variable, variable_);
console.log(res); // false
</script>
<script>
console.log(+0 === -0); //true
console.log(NaN === NaN); // false
console.log(Object.is(+0, -0)); // false
console.log(Object.is(NaN, NaN)); // true
</script>
2 Object.assign()
开发中常能见到,这个方法还是要着重了解的,需要注意的就是此方法为:** 浅拷贝 **
[ Object.assign() ] 方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target).
<script>
const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
console.log(target); // {a: 1, b: 2, c: 3}
</script>
[ Object.assign() ] 方法的第一个参数是目标对象,后面的参数都是源对象.
注意: 如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性.
<script>
const target = {a: 1, b: 1};
const source1 = {b: 2, c: 2};
const source2 = {c: 3};
Object.assign(target, source1, source2);
console.log(target); // {a:1, b:2, c:3}
</script>
如果只有一个参数, [ Object.assign() ] 会直接返回该参数.
<script>
const obj = {a: 1};
console.log(Object.assign(obj) === obj); // true
</script>
如果该参数不是对象,则会先转成对象,然后返回.
<script>
console.log(typeof Object.assign(2)); // "object"
</script>
由于 undefined 和 null 无法转成对象,所以如果它们作为参数,就会报错.
Object.assign(undefined) // 报错
Object.assign(null) // 报错
如果非对象参数出现在源对象的位置(即非首参数),那么处理规则有所不同. 首先,这些参数都会转成对象,如果无法转成对象,就会跳过. 这意味着,如果 undefined 和 null 不在首参数,就不会报错.
let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true
其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错. 但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果.
<script>
const v1 = 'abc';
const v2 = true;
const v3 = 10;
const obj = Object.assign({}, v1, v2, v3);
console.log(obj); // { "0": "a", "1": "b", "2": "c" }
</script>
上面代码中,v1、v2、v3分别是字符串、布尔值和数值,结果只有字符串合入目标对象(以字符数组的形式),数值和布尔值都会被忽略. 这是因为只有字符串的包装对象,会产生可枚举属性.
<script>
console.log(Object(true));
console.log(Object(10));
console.log(Object('abc'));
</script>
</body>
上面代码中,布尔值、数值、字符串分别转成对应的包装对象,可以看到它们的原始值都在包装对象的内部属性[[PrimitiveValue]]上面,这个属性是不会被 [ Object.assign() ] 拷贝的. 只有字符串的包装对象,会产生可枚举的实义属性,那些属性则会被拷贝.
[ Object.assign() ] 拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false).
上面代码中, [ Object.assign() ] 要拷贝的对象只有一个不可枚举属性invisible,这个属性并没有被拷贝进去.
属性名为 Symbol 值的属性,也会被 [ Object.assign() ] 拷贝.
浅拷贝
[ Object.assign() ] 方法实行的是浅拷贝,而不是深拷贝. 也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用.