JavaScript (11) ES6语法

1. ES6 语法

ECMAScript 6 是继ECMAScript 5 之后发布的JavaScript 语言的新一代标准,加入了很多新的特性和语法,该标准于2015年6月17日发布了正式版本,并被正式命名为ECMAScript 2015。Vue项目开发中经常会用到ECMAScript 6语法,因此接下来将对这一新标准中的一些特性和常用语法进行简要介绍。

1.1 块作用域let和const

块级声明用于声明在指定块的作用域之外无法访问的变量。块级作用域存在于:

  • 函数内部
  • 块中(也就是“{”和“}”之间的区域)

1.1.1 let

通过关键字 var 来声明的变量,不管是在函数中还是在全局中声明,都会被当成在当前作用域顶部声明的变量,这就是JavaScript的变量提升机制。

<script>
    function show(flag) {
        if (flag) {
            let name = "jock";
        } else {
            console.log(name);
        }
    }
    show(false);

    {
        let num = 5;
        console.log(num);
    }

    //console.log(num); // 报错

    for (let i = 0; i < 5; i++) {

    }
    //console.log(i); // 报错
    let a = 'b';
    //let a = 'c' // 不能再重复声明变量了
</script>

1.1.2 const

ECMAScript 6还提供了const关键字,用于声明常量。每个通过const关键字声明的常量必须在声明的同时进行初始化。

<script>
    const age = 18;
    console.log(age);
    //age = 20; // 报错

    const obj = {
        name: '张三',
        age : 18
    }
    console.log(obj);
    // obj = { } // 报错
    obj.name = '李四'
    console.log(obj);
</script>

1.2 模板字面量

ECMAScript6引入了模板字面量(Template Literals),对字符串的操作进行了增强:

  • 多行字符串
  • 字符串占位符

可以将变量或JavaScript表达式嵌入占位符中并将其作用为字符串的一部分输出到结果中。

1.2.1 多行字符串

使用(``)来指定多行字符串。

<script>
    let str = 'hello \ntemplate \nliterals';
    console.log(str);

    str = `hello
     template
      literals`
    console.log(str);
</script>

1.2.2 字符串点位符

在模板字面量中,可以将JavaScript变量或者任何合法的JavaScript表达式嵌入占位符中,并将其作为字符串的一部分输出到结果中。占位符由一个左侧的“${”和右侧的”}“组成,中间可以包含变量以及表达式。

let name = '天子';
let message = `hello ${name}`;
console.log(message);

1.3 默认参数

在ECMAScript 6之前,没有提供在函数的参数列表中指定参数默认值的语法,想要为函数参数指定默认值,只能通过下面的模式来为参数指定默认值。

function say(name, age) {
    name = (typeof name != 'undefined') ? name : 'jock';
    age = (typeof age != 'undefined') ? age : 18;
    console.log(name, age);
}
say();
say('jock')
say('jock', 20);

// ES6
function say2(name = 'jock', age = 18) {
    console.log(name, age);
}
say2();
say2('jock')
say2(undefined, 30);
say2('jock', 20);
//当给有默认值参数传入undefined时,才会使用默认值

1.4 解构赋值

在JavaScript中,经常需要从某个对象或数组中提取特定的数据赋给变量,如。

let person = {
    name : '刘备',
    gender: '男',
    age : 25
}
let {name, age, gender} = person
console.log(name, age, gender);

对于解构对象来说,解析所使用的变量名称要与对象中的属性名相同 ,它们顺序可以不同。

let arr = [1, 2, 3, 4];
let [a,b,,c] = arr;
console.log(a, b, c);

对于解构数组,变量的顺序要与数组中的元素顺序相同。

const [e,f,g] = 'hello';
console.log(e, f, g);

对于解构字符串,也是需要注意顺序。

    function add([a, b]) {
        return a + b;
    }
    console.log(add([2, 3]));

还可以解构数组参数,它的使用方式与解构数组相同。

1.5 rest 运算符

解构会将相同数据结构对应的值赋给对应的变量,但是当我们想将其中的一部分值统一赋值给一个变量时,可以使用 rest 运算符。

首先来看看 rest 运算符和数组解构相关的内容。

let arr1 = ['a', 'b', 'c', 'd'];
let arr2 = arr1;
console.log(arr2)
arr1[4] = 'e';
console.log(arr2)

let [...arr3] = arr1;
arr1[5] = 'f';
console.log(arr3);
//说明:上面的代码是把 arr1 数组中的元素打散后赋给 arr3 数组。
//rest还可以替换函数中的 arguments 对象来处理参数。

function add() {
    //let r = a + b;
    let sum = 0;
    for (let i = 0; i < arguments.length; i++) {
        sum += arguments[i];
    }
    return sum;
}
const r = add(1,2,3,4,5);
console.log(r);

使用 rest 替换后:

function add1(...args) {
    let sum = 0;
    for (let i = 0; i < args.length; i++) {
        sum += args[i];
    }
    return sum;
}
const r1 = add1(1,2,3,4,5);
console.log(r1);

1.6 展开运算符

展开运算符也叫扩展运算符,在语法上与rest参数相似,也是三个点(),它可以将一个数组转换为各个独立的参数,也可用于取出对象的所有可遍历属性,而rest参数是让你指定多个独立的参数,并通过整合后的数组来访问。

  1. 展开运算符(…)提取数组arr中的各个值并传入add函数中。

        function add(a, b, c) {
            return a + b + c;
        }
        let arr = [1, 2, 3];
        let result = add(...arr); // 此处使用了展开运算符
        console.log(result);
    
  2. 展开运算符可以用来复制数组。

    let arr1 = [1,2,3];
    let arr2 = arr1;
    let arr3 = [...arr1]; // 此处使用了展开运算符
    arr1[0] = 4;
    console.log(arr2[0]);
    console.log(arr3[0]);
    

    从上面代码可以看出,如果需要复制数据,可以使用展开运算符来实现。

  3. 展开运算符也可以用来合并数组

    let arr4 = ['hello', ','];
    let arr5 = ['world', '.'];
    let arr6 = [...arr4, ...arr5];
    console.log(arr6);
    
  4. 展开运算符还可以取出对象的所有可遍历的属性复制到当前对象中

    let person = {
        name: '小张',
        age: 18,
    }
    let detail = {
        ...person,
        gender: '男',
    }
    console.log(detail);
    
  5. 拆解字符串

    const str = 'hello';
    console.log(str);
    console.log(...str);
    

1.6.1 代替 apply 函数

扩展运算符可以代替 apply() 函数,将数组转换为函数参数。

例如,获取数组最大值时,使用 apply() 函数的写法如下:

let arr = [1,5,2,29,12,8];
const r = Math.max.apply(null, arr);
console.log(r);

使用展开运算符后的写法如下:

const r1 = Math.max(...arr);
console.log(r1);

1.6.2 代替 concat 函数

在 ES5 中,合并数组时需要使用 concat() 函数:

let arr1 = [1,2,3];
let arr2 = [4,5,6];
console.log(arr1.concat(arr2));

使用展开运算符后:

let arr1 = [1,2,3];
let arr2 = [4,5,6];

let arr3 = [...arr1, ...arr2];
console.log(arr3);

1.7 字符串新增的方法

1.7.1 startsWith()

startsWith() 方法用来判断当前字符串是否以另外一个给定的子字符串开头,并根据判断结果返回 true 或 false。语法如下:

str.startsWith(searchString[, position])
参数说明
searchString必须,要搜索的子字符串
position可选,在 str 中搜索 searchString 的开始位置,默认值为 0

示例:

const str = 'meta char set UTF 8';
let b = str.startsWith('me');
b = str.startsWith('mea');
console.log(b);

b = str.startsWith('char', 5);
console.log(b);

除了 startWith() 方法外,还有一个功能相同的 endWith() 方法,它是用于判断是什么子字符结尾的。

1.7.2 endsWith()

endsWith() 方法用来判断当前字符串是否以另外一个给定的子字符串结尾,并根据判断结果返回 true 或 false。语法如下:

str.endsWith(searchString[, position])
参数说明
searchString必须,要搜索的子字符串
position可选,在 str 中搜索 searchString 的结束位置,默认值为 0

示例:

const str = 'meta char set UTF 8';
let b = str.endsWith('me');
b = str.endsWith('8');
console.log(b);

b = str.endsWith('char', 9);
console.log(b);

1.7.3 includes()

includes() 方法用来判断当前字符串是否包含在指定字符串中,并根据判断结果返回 true 或 false。语法如下:

str.includes(searchString[, position])
参数说明
searchString必须,要搜索的子字符串
position可选,在 str 中搜索 searchString ,默认值为 0

示例:

let str = 'Hello World';
let b = str.includes('llo');
console.log(b);
b = str.includes('Wo', 5);
console.log(b);

注意:在搜索时是区分大小写的。

1.7.4 padStart()和padEnd()

ES 2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart() 用于头部补全,padEnd() 用于尾部补全。

console.log('1'.padStart(10, '0'));
console.log('1000000000'.padStart(10, '0'));

console.log('1'.padEnd(5, '0'));
console.log('10000'.padEnd(5, '0'));

说明:从上面的使用可以发现,这两个方法有两个参数:第一个参数是补全后的长度,第二个参数是需要补全的字符串。

1.7.5 trimStart()和trimEnd()

ES 2019 对字符串补全新增了 trimStart() 和 trimEnd() 这两个方法。它们的行为与 trim() 一致,trimStart() 消除字符串头部的空格,trimEnd() 消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。

const s = '   abc     '
console.log(s);
console.log(s.trim());
console.log(s.trimStart());
console.log(s.trimEnd());

1.8 数值新增方法

1.8.1 Number.isInteger()

Number.isInteger() 用来判断一个数值是否为整数。

console.log(Number.isInteger(25));
console.log(Number.isInteger(25.1));

JavaScript 内部,整数和浮点数采用的是同样的储存方法,所以 25 和 25.0 被视为同一个值。

console.log(Number.isInteger(25));
console.log(Number.isInteger(25.0));

如果参数不是数值,Number.isInteger 返回 false。

console.log(Number.isInteger(null));
console.log(Number.isInteger());
console.log(Number.isInteger('1'));
console.log(Number.isInteger(true));
console.log(Number.isInteger(undefined));

以下是特殊情况:

console.log(Number.isInteger(3))
console.log(Number.isInteger(3.0))
console.log(Number.isInteger(3.000000000000002)) // false
console.log(Number.isInteger(3.0000000000000002)) // true

原因是这个小数的精度达到了小数后的 16 个十进制位,转成二进制后,超过了 53 个二进制位,导致最后的 2 被丢弃。

1.8.2 Math.trunc()

Math.trunc() 方法是用于去除一个数的小数部分,返回整数部分。

console.log(Math.trunc(4.2));
console.log(Math.trunc(4.8));
console.log(Math.trunc(-4.8));
console.log(Math.trunc(0.123));

如果给的参数是非数字的,那么它会试着先转换为数值后再进行处理。

console.log(Math.trunc('123.4'));
console.log(Math.trunc(true));
console.log(Math.trunc(null));

如果给定的参数是空值或不能转换的值,则返回 NaN。

console.log(Math.trunc());
console.log(Math.trunc(NaN));
console.log(Math.trunc('hello'));
console.log(Math.trunc(undefined));

1.8.3 Math.sign()

Math.sign() 方法用于判断一个数是正数、负数还是零。对于非数值,会先将其转换为数值。

  • 参数为正数,返回 +1
  • 参数为负数,返回 -1
  • 参数为零,返回 0
  • 参数为 -0,返回 -0
  • 参数为其它值,返回 NaN
console.log(Math.sign(6));
console.log(Math.sign(-6));
console.log(Math.sign(0));
console.log(Math.sign(-0));
console.log(Math.sign(+0));
console.log(Math.sign(NaN));
console.log(Math.sign(null));
console.log(Math.sign('1'));
console.log(Math.sign('a'));

1.9 函数扩展

1.9.1 参数默认值

1.9.1.1 基本使用

有 ES6 之前,没有提供在函数的参数列表中的默认值,想使用默认值,我们需要做如下的操作:

function show(name) {
    name = name || '天子';
    console.log(name);
}

show();
show('天子1');

在 ES6 中,我们可以使用默认参数

function show(name='天子') {
    console.log(name);
}

show();
show('天子1');

注意:当使用了默认参数后,在函数中就不能再次去声明这个变量了。

function show(name='天子') {
    //let name = '李四'; // 报错
    //const name = '王五'; // 报错
    console.log(name);
}

下面的代码中,参数 p 的默认值是 x + 1。这时,每次调用函数 fun() 都会重新计算 x + 1 的值,而不是默认 p 的值等于 10。

let x = 9;
function fun(p = x + 1) {
    console.log(p);
}
fun(); // 10
x = 10;
fun(); // 11
1.9.1.2 默认值与解构赋值结合使用
function foo({x, y = 5}) {
    console.log(x, y);
}
foo({}); // undefined 5
foo({x:1}); // 1 5
foo({x:2, y:3}); // 2 3
//foo(); // 报错

如果希望在上面的代码的报错地方不会报错应该怎么办?

function foo({x, y = 5} = {}) {
    console.log(x, y);
}
foo({}); // undefined 5
foo({x:1}); // 1 5
foo({x:2, y:3}); // 2 3
foo(); // undefined 5
1.9.1.3 参数默认值的位置

通常情况下,定义默认值是在参数列表的最后,这样方便我们查看和使用。但是,我们也可以把默认值放到参数列表的任意位置。

function f(x=1, y) {
    return [x, y];
}

console.log(f()); // [1, undefined]
console.log(f(2)); // [2, undefined]
console.log(f(2,3)); // [2, 3]
//console.log(f(,3)); // 报错
console.log(f(null,3)); // [null, 3]
console.log(f(undefined,3)); // [1, 3]

如果默认参数不是在最后,那么在传参数的时候,默认参数所在的位置还是需要传参。这个参数如果想使用默认参数的值,则应该传 undefined 。

使用 undefined 可以使用默认值。

1.9.1.4 函数的 length 属性

指定了默认值后,函数的 length 属性将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length 属性将失效。

console.log((function f(a, b) {}).length); // 2
console.log((function f(a, b=1) {}).length); // 1
console.log((function f(a=2, b=1) {}).length); // 0

1.9.2 箭头函数

ES6 允许使用箭头(=>)定义函数,箭头函数的语法多变,根据实际的使用场景有多种形式,但都需要由函数参数、箭头和函数体组成。根据JavaScript函数定义的各种不同形式,箭头函数的参数和函数体可以分别采取多种不同形式。

基本结构如下:

(参数列表) => {函数值;}
1.9.2.1 基本使用

1.如果参数只有一个,则函数的的圆括号可以省略。

function fun(a) {
    console.log(a);
}
fun(1);
// 转换为箭头函数
let f = a => {
    console.log(a);
}
f(3);

2.如果函数体中只有一条语句,那么大括号可以省略。

let f = a => console.log(a);
f(4);

3.如果函数体中只有一条 return 语句,那么大括号和 return 关键字都可以省略。

function fun1(a) {
    return a;
}
console.log(fun1(10));

let f1 = a => a;
console.log(f1(20));

4.如果参数列表为空,那么圆括号不能省略。

function f2() {
    console.log('hello')
}
f2();
let f3 = () => console.log('hello')
f3();

注意:如果参数列表中有多个参数,那么圆括号不能省略;如果函数体中有多条语句时,大括号不能省略。

1.9.2.2 箭头函数与this

JavaScript 中的 this 关键字是一个神奇的东西,与其他高级语言中的 this 引用或 this 指针不同的是,JavaScript 中的 this 并不是指向对象本身,其指向是可以改变的,根据当前执行上下文的变化而变化。

var greeting = 'Welcome'
function sayHi(msg) {
    alert(this.greeting + "," + msg)
}
var obj = {
    greeting : 'Hi',
    sayHi : sayHi
}
sayHi('jock')   // Welcome, jock
obj.sayHi('jock')   // Hi, jock
var say = obj.sayHi;
say('jock')   // Welcome, jock

说明:在 ES6 之前,this 的指向是根据上下文环境来发生变化的。在 ES6 中的箭头函数中,this 批向的就是全局对象。

var greeting = 'Welcome'
let sayHi = msg => alert(this.greeting + "," + msg)
var obj = {
    greeting : 'Hi',
    sayHi : sayHi
}
sayHi('jock')   // Welcome, jock
obj.sayHi('jock')   // Welcome, jock
var say = obj.sayHi;
say('jock')   // Welcome, jock
1.9.2.3 注意事项

箭头函数有几个使用注意点:

1)箭头函数没有自己的 this 对象

2)不可以当作构造函数,也就是说,不可以对箭头函数使用 new 命令,否则会抛出一个错误

3)不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替

4)不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。

上面四点中,最重要的是第一点。对于普通函数来说,内部的 this 指向函数运行时所在的对象,但是这一点对箭头函数不成立。它没有自己的 this 对象,内部的 this 就是定义时上层作用域中的 this。也就是说,箭头函数内部的 this 指向是固定的,相比之下,普通函数的 this 指向是可变的。

1.9.2.4 使用场景

由于箭头函数使得 this 从动态变成静态,下面两个场合不应该使用箭头函数。

第一个场合是定义对象的方法,且该方法内部包括 this。

const cat = {
    lives: 9,
    jumps: () => {
        this.lives--;
    }
}

上面代码中,cat.jump() 方法是一个箭头函数,这是错误的。调用 cat.jump() 时,如果是普通函数,该方法内部的 this 指向 cat;如果写成上面那样的箭头函数,使得 this 指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致 jumps 箭头函数定义时的作用域就是全局作用域。

再看一个例子:

globalThis.s = 21;

const obj = {
    s: 42,
    m: () => console.log(this.s)
};

obj.m(); // 21

上面例子中,obj.m() 使用箭头函数定义。JavaScript 引擎的处理方法是:先在全局空间生成这个箭头函数,然后赋值给 obj.m,这导致箭头函数内部的 this 指向全局对象,所以 obj.m() 输出的是全局空间的 21,而不是对象内部的 42。上面代码实际上等同于下面的代码:

globalThis.s = 21;
globalThis.m = () => console.log(this.s);

const obj = {
    s: 42,
    m: globalThis.m
};

obj.m();

由于上面这个原因,对象的属性建议使用传统的写法定义,不要用箭头函数定义。

第二个场合是需要动态 this 的时候,也不应该使用箭头函数。

var button = document.querySelector("#press");
button.addEventListener('click', ()=>{
    this.classList.toggle('on');
})

上面代码运行时,点击按钮会报错,因为 button 的监听函数是一个箭头函数,导致里面的 this 就是全局对象。如果改成普通函数,this 就会动态指向被点击的按钮对象。

另外,如果函数体很复杂,有许多行,或者函数内部有大量的读写操作,不单纯是为了计算值,这时也不应该使用箭头函数,而是要使用普通函数,这样可以提高代码可读性。

1.10 数组扩展

1.10.1 Array.from()

Array.from() 方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。

下面是一个类似数组的对象,Array.from() 方法将它转为真正的数组。

let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
}

// ES5
var arr1 = [].slice.call(arrayLike);
console.log(arr1);

// ES6
let arr2 = Array.from(arrayLike);
console.log(arr2);

当给 Array.from() 一个数组参数时,它会返回一个一模一样的数组。

let arr3 = Array.from([1,2,3]);
console.log(arr3);

1.10.2 Array.of()

Array.of() 方法用于将一组值转换为数组。

let arr1 = Array.of(1,3,5,7,9);
console.log(arr1, typeof arr1);

这个方法的主要目的是弥补数组结构函数 Array() 不足。

let arr3 = new Array();
console.log(arr3);

let arr4 = new Array(5);
console.log(arr4);

let arr5 = new Array(5,1);
console.log(arr5);

说明:Array.of() 方法基本上可以用来替换 new Array() 创建方式。

Array.of() 方法的源码大概如下:

function ArrayOf() {
    return [].slice.call(arguments);
}

1.10.3 fill() 方法

fill() 方法用于将给定的值填充到一个数组中。

let arr1 = [1,2,3];
console.log(arr1);
arr1.fill('a');  // [a, a, a]
console.log(arr1);

console.log(new Array(3).fill(7)); // [7, 7, 7]

fill 方法用于空数组的初始化操作。如果数组中已经有元素,会被全部替换为指定值。

fill 方法还可以接收第二个参数和第三个参数,用于指定填充的起始位置和结束位置。

let arr2 = ['a', 'b', 'c', 'd', 'e'];
arr2.fill(5, 2, 4)
console.log(arr2); // ['a', 'b', 5, 5, 'e']

说明:如果只有第二个参数没有第三个参数,那么会从第二个参数位置开始到最后都会被替换掉。如果第三个参数和第三个参数都指定了,那么会从第二个参数位置开始到第三个参数位置之前结束。

1.10.4 flat() 和 flatMap()

如果数组成员还是数组,Array.prototype.flat() 方法来把嵌套的数组拉平。这个方法会返回一个新的数组,对原数组没有影响。

    let arr1 = [1, 2, 3, [4, 5, 6], [7, 8, 9]];
    console.log(arr1);

    let arr2 = arr1.flat();
    console.log(arr2);

    let arr3 = [1, 2, 3, [4, 5, 6, [10, 11, 12]], [7, 8, 9]];
    console.log(arr3);
    let arr4 = arr3.flat(2);
    console.log(arr4);

说明:flat() 方法可以把二维数组转换为一维数组。如果想把三维数组转换为一维数组,那么就需要给这个方法指定一个数值类型的参数,用于表示拉平的深度,默认为 1。

flatMap() 方法对原数组的每个成员执行一个函数,相当于执行 Array.prototype.map() ,然后返回值组成一个数组后再执行 flat() 方法。该方法返回一个新数组,不改变原来的数组。

let arr1 = [2, 3, 4];
let arr2 = arr1.flatMap(x => {
    //console.log(x, x * 2, [x, x * 2]);
    return [x, x * 2]
})
// let arr2 = [[2,4], [3,6], [4,8]];
// let arr3 = arr2.flat();

console.log(arr2);

分析:flatMap() 只会做两步操作,第一步是相当于执行 map() 方法,第二步是将第一步执行后的结果再执行 flat() 方法来拉平。

注意:flatMap() 只能展开一层数组。

let arr1 = [2, 3, 4, [5, 6]];
let arr2 = arr1.flatMap(x => [x, x * 2])
console.log(arr2); // [2, 4, 3, 6, 4, 8, Array(2), NaN]

1.11 对象扩展

1.11.1 属性简写

在 ES6 中,如果对象中的属性名和属性值相同,则可以简写。

const name = '天子';
const age = 18;
// ES5中的写法
const obj = {
    name: name,
    age: age,
}
console.log(obj);

// ES6中的写法
const obj1 = {
    name,
    age,
}
console.log(obj1);

如果属性的值是一个函数,那么这个属性也可以简写:

// ES5中的写法
const obj = {
    method: function () {
        console.log('haha');
    }
}
obj.method();

// ES6中的写法
const obj1 = {
    method() { // 简写
        console.log('hehe')
    }
}
obj1.method()

1.11.2 对象遍历

在 ES6 中,对象遍历一共有 5 种,具体方法如下:

1. for...in
2. Object.keys(obj);
3. Object.getOwnPropertyNames(obj);
4. Object.getOwnpPropertySymbols(obj);
5. Reflect.ownKeys(obj);

下面以示例来进行演示:

首先创建需要的对象和属性

// 定义父类
function Animal(name, type) {
    this.name = name;
    this.type = type;
}
// 定义子类
function Cat(age, weight) {
    this.age = age;
    this.weight = weight;
    this[Symbol('one')] = 'one';
}
// 子类继承父类
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

// 生成子类实例对象
let cat = new Cat(5, '10kg');

// 对实例对象增加可枚举属性
Object.defineProperty(cat, 'color', {
    configurable: true,
    enumerable: true, // 可枚举
    value: 'blue',
    writable: true
})
// 对实例对象增加不可枚举属性
Object.defineProperty(cat, 'height', {
    configurable: true,
    enumerable: false, // 不可枚举
    value: '20cm',
    writable: true
})

1.使用for…in 遍历

for…in 用于遍历对象自身的属性和继承的可枚举属性(不包含 Symbol 属性)

for (let key in cat) {
    console.log(key);
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zladoBrp-1651665362298)(image/image-20220210150456391.png)]

2.Object.keys(obj)

这个方法返回一个数组,包含对象自身所有可枚举属性,不包含继承属性和 Symbol 属性。

console.log(Object.keys(cat));

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SMBym9C8-1651665362300)(image/image-20220210150726948.png)]

3.Object.getOwnPropertyNames(obj);

这个方法返回一个数组,包含对象自身的可枚举属性和不可枚举属性,不包含继承和 Symbol 属性。

console.log(Object.getOwnPropertyNames(cat));

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0nlwBmsO-1651665362300)(image/image-20220210150946135.png)]

4.Object.getOwnPropertySymbols()

这个方法返回一个数组,包含对象自身的所有 Symbol 属性,不包含其它属性。

console.log(Object.getOwnPropertySymbols(cat));

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PIWRiaaC-1651665362301)(image/image-20220210151134776.png)]

5.Reflect.ownKeys(obj)

这个也返回一个数组,可包含枚举属性,不可枚举属性以及 Symbol 属性,不包含继承属性。

console.log(Reflect.ownKeys(cat));

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V0o8LvTg-1651665362301)(image/image-20220210151318505.png)]

注意:对于 for…in 遍历来说,如果是遍历数组的话,得到的 key 是数组元素的下标。

let arr = [1, 3, 5, 7, 9];
for (let k in arr) {
    console.log(k, arr[k]);
}

如果想直接获取到数组的元素则需要使用 for…of 方法。

let arr = [1, 3, 5, 7, 9];
for (let v of arr) {
    console.log(v);
}

for…of 中的循环变量指向的就是数组中的当前元素。

对于对象来说,我们要使用 for…in 来进行遍历,不要使用 for…of 来遍历。对于数组我们尽量使用 for…of 来遍历,不要使用 for…in 来遍历。

const obj = {
    name: '张三',
    age: 18,
}
for (let k in obj) {
    console.log(k);
}

for (let v of obj) {
    console.log(v); // 报错
}

1.12 Proxy

1.12.1 概述

ES6 中新增了 Proxy 对象,从字面上看可以理解为代理器,主要用于改变对象的默认访问行为,实际表现是在访问对象之前增加一层拦截,任何对对象的访问行为都会通过这层拦截。在拦截中,可以增加自定义的行为。

在了解 Proxy 的代理行为后,我们来学习 Proxy 的代码表现形式。

https://developer.mozilla.org/zh-CN/

Proxy 的基本语法如下:

const p = new Proxy(target, handler)

参数

  • target

    要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。

  • handler

    一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

Proxy,target 和 handler 之间的关系是什么样的呢?

通过 Proxy 构造函数可以生成实例 proxy,任何对 proxy 实例的属性访问都会自动转发到 target 对象上,我们可针对访问的行为配置自定义的 handler 对象,因此外界通过 proxy 访问 target 对象的属性时,都会执行 handler 对象自定义的拦截操作。

// 定义目标对象
const person = {
    name: '张三',
    age: 18,
}

// 定义处理对象
const handler = {
    get: function (target, prop, receiver) {
        console.log('你访问了 ' + target + ' 的 ' + prop + ' 属性');
        return target[prop];
    }
}

// 生成 Proxy 实例对象
const proxy = new Proxy(person, handler);

// 获取 person 对象中的 name 属性
console.log(proxy.name);

在执行 proxy.name 时,会去执行 handler 中定义的 get 方法,因此就可以在这个方法中添加一些额外功能来增强。

1.12.2 Proxy实例函数及其基本使用

通过访问代理对象的属性来触发自定义配置对象的 get() 函数,而 get() 函数只是 Proxy 实例支持的总共 13 种函数中的一种,这 13 种函数汇总如下。

1、get(target, propKey, receiver)

拦截对象属性的读取操作,例如调用 proxy.name 或者 proxy[name],其中 target 表示是目标对象,propKey 表示的是读取的属性值,receiver 表示的是配置对象。

2、set(target, propKey, value, receiver)

拦截对象属性的写操作,即设置属性值,例如 proxy.name = ‘jock’ 或者 proxy[name] = ‘jock’,其中 target 表示目标对象,propKey 表示是将要设置的属性,value 表示将要设置属性的值,receiver 表示的是配置对象。

3、has(target, propKey)

拦截 hasProperty 操作,返回一个布尔值,最典型的表现形式是执行 propKey in target,其中 target 表示目标对象,propKey 表示判断的属性。

4、deleteProperty(target, propKey)

拦截 delete proxy[propKey] 的操作,返回一个布尔值,表示是否执行成功,其中 target 表示目标对象,propKey 表示将要删除的属性。

5、ownKeys(target)

拦截 Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in 循环等操作,其中 target 表示的是获取对象自身所有的属性名。

6、getOwnPropertyDescriptor(target, propKey)

拦截 Object.getOwnPropertyDescriptor(target, propKey) 操作,返回属性的属性描述符构成的对象,其中 target 表示目标对象,propKey 表示需要获取属性描述符集合的属性。

7、defineProperty(target, propKey, propDesc)

拦截 Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propKey, propDesc) 操作,返回一个布尔值,其中 target 表示目标对象,propKey 表示新增的属性,propDesc 表示的是属性描述符对象。

8、preventExtensions(target)

拦截 Object.preventExtensions(proxy) 操作,返回一个布尔值,表示的是让一个对象变得不可扩展,不能再增加新的属性,其中 target 表示目标对象。

9、getPrototypeOf(target)

拦截 Object.getPrototypeOf(target) 操作,返回一个对象,表示的是拦截获取对象原型属性,其中 target 表示目标对象。

10、isExtensible(target)

拦截 Object.isExtensible(proxy),返回一个布尔值,表示对象是否是可扩展的,其中 target 表示目标对象。

11、serPrototypeOf(target, proto)

拦截 Object.setPrototypeOf(proxy, proto) 操作,返回一个布尔值,表示的是拦截设置对象的原型属性的行为。其中 target 表示目标对象,proto 表示新的原型对象。

12、apply(target, object, args)

拦截 Proxy 实例作为函数调用的操作,例如 proxy(argsproxycall(obj …args))、其中 target 表示目标对象,object 表示函数的调用方,args 表示函数调用传递的参数。

13、construct(target, args)

拦截 Proxy 实例作为构造函数调用的操作。例如 new proxy(…args),其中 target 表示目标对象,args 表示函数调用传递的参数。

const person = {
    name: '张三',
    getName() {
        console.log(this === proxy);
    }
}
const proxy = new Proxy(person, {
    get: function (target, prop, receiver) {
        console.log('你访问了 ' + prop + ' 属性')
        return target[prop];
    },
    set: function (target, prop, value, receiver) {
        console.log('你修改了' + prop + ' 属性的值')
        target[prop] = value;
        //Reflect.set(target, prop, value, receiver);
    },
    ownKeys(target) {
        return Reflect.ownKeys(target);
    }
})

console.log(proxy.name);
proxy.name = '李四'
console.log(proxy.name);
proxy.getName()

// person.getName(); // 不要直接访问对象,要通过代理对象来进行访问

for (let key in proxy) {
    console.log(key);
}

1.12.3 读取不存在的属性

在正常情况下,读取一个对象不存在的属性时,会返回 undefined,通过 Proxy 的 get() 函数可以设置读取不存在的属性时抛出异常,从而避免对 undefined 值得兼容性处理。

const person = {
    name: 'jock',
}
const proxy = new Proxy(person, {
    get(target, p, receiver) {
        if (p in target) {
            return target[p];
        } else {
            throw new ReferenceError(`访问的属性 ${p} 不存在!`)
        }
    }
})
console.log(proxy.name);
console.log(proxy.age);

1.12.4 读取负索引的值

数组的索引值是从 0 开始依次递增的,正常情况下我们无法读取负索引值,但是通过 Proxy() 的 get() 函数可以做到这一点。

const arr = [1, 4, 9, 20];
const proxy = new Proxy(arr, {
    get(target, index) {
        // 注意:获取到的参数是字符串类型
        index = Number(index);
        //console.log(index, typeof index)
        if (index > 0) {
            return target[index];
        }
        // 当索引值为负数时,从数组的尾部开始获取
        return target[target.length + index];
    }
})
console.log(proxy[2]);
console.log(proxy[-3]);

1.13 Reflect

1.13.1 概述

Reflect 对象与 Proxy 对象一样,也是 ES6 为了操作对象而提供的新 API。

那么什么是 Reflect 对象呢?

我们可以这样理解:有一个名为 Reflect 的全局对象,上面挂载了对象的某些特殊函数,这些函数可以通过类似于 Reflect.apply() 这种形式来调用,所有在 Reflect 对象上的函数要么可以在 Object 原型链中找到,要么可以通过命令式操作符实现,例如 delete 和 in 操作符。

大家可能会有疑问,既然在 ES6 之前,Object 对象中已经有与 Reflect 的函数相同功能的函数或者命令式操作符,那么为什么还要在 ES6 中专门增加一个 Reflect 对象呢?

主要原因有以下几点:

  1. 更合理地规划与 Object 对象相关的 API。在 ES6 中,Object 对象的一些明显属于语言内部的函数都会添加到 Reflect 对象中,这样 Object 对象与 Reflect 对象中会存在相同的处理函数。而在未来的设计中,语言内部的函数将只会添加到 Reflect 对象中。
  2. 用一个单一的全局对象去存储这些函数,能够保持其它的 JavaScript 代码的整洁、干净。不然的话,这些函数可能是全局的,或者要通过原型来调用,不方便统一管理。
  3. 将一些命令式的操作符如 delete、in 等使用函数来替代,这样做的目的是为了让代码更好维护,更容易向下兼容,同时也避免出现更多的保留字。
  4. 修改 Object 对象的某些函数的返回结果,可以让其变得更合理、使代码更好维护。

如果一个对象是不能扩展的,那么在调用 Object.defineProperty(obj, name, desc) 时,会抛出一个异常,因此在传统的写法中,需要通过 try…catch 处理。

而使用 Reflect.defineProperty(obj, name, desc) 时,返回的是 false,新的写法就是可以通过 if…else 实现。

// 以前的写法
try {
   Object.defineProperty(obj, name, desc) 
} catch(e) {
    
}

// 新的写法
if (Reflect.defineProperty(obj, name, desc)) {
    
} else {
    
}

1.13.2 静态方法

1、Reflect.apply(target, thisArgument, argumentsList)

对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和 Function.prototype.apply() 功能类似。

2、Reflect.constructor(target, argumentsList[, newTrarget])

对构造函数进行 new 操作,相当于执行 new target(…args)。

3、Reflect.defineProperty(target, propertyKey, attributes)

和 Object.defineProperty() 类似,如果设置成功就返回 true。

4、Reflect.deleteProperty(target, propertyKey)

作为函数的 delete 操作符,相当于 delete target[name]。

5、Reflect.get(target, propertyKey[, receiver])

获取对象身上某个属性的值,类似于 target[name]。

6、Reflect.getOwnPropertyDescriptor(target, propertyKey)

类似于 Object.getOwnPropertyDescriptor(),如果对象中存在该属性,则返回对应的属性描述符,否则返回 undefined。

7、Reflect.getPrototypeOf(target)

类似于 Object.getPrototypeOf()。

8、Reflect.has(target, propertyKey)

判断一个对象是否存在某个属性,和 in 运算符的功能完全相同。

9、Reflect.isExtensible(target)

类似于 Object.isExtendsible()。

10、Reflect.ownKeys(target)

返回一个包含所有自身属性(不包含继承属性)的数组。类似于 Object.keys(),但不会受 enumerable 影响。

11、Reflect.preventExtensions(target)

类似于 Object.preventExtensions(),返回一个布尔值。

12、Reflect.set(target, propertKey, value[,receiver])

将值分配给属性的函数。返回一个 Boolean,如果更新成功,则返回 true。

13、Reflect.setPrototypeOf(target, prototype)

设置对象原型的函数,返回一个布尔值,如果更新成功则返回 true,否则返回 false。

1.13.3 Reflect.has()

Reflect.has() 方法对应 name in obj 里面的 in 运算符。

const obj = {
    name: '张三'
}
// 旧的写法
console.log('name' in obj);
console.log('name1' in obj);
// 新的写法
console.log(Reflect.has(obj, 'name'));
console.log(Reflect.has(obj, 'name1'));

注意:如果 Reflect.has() 方法的第一个参数不是对象会报错。

const obj2 = 3;
console.log(Reflect.has(obj2, 'name')); // Uncaught TypeError: Reflect.has called on non-object

1.13.4 Reflect.deleteProperty()

Reflect.deleteProperty(obj, prop) 方法等同于 delete obj[prop],用于删除对象的属性。

const obj1 = {
    foo: 'foo',
    bar: 'bar'
}
console.log(obj1);

// 旧写法
delete obj1["foo"];
console.log(obj1);

// 新写法
Reflect.deleteProperty(obj1, 'bar')
console.log(obj1);

Reflect.deleteProperty() 方法会返回一个布尔值。如果删除成功或删除的属性不存在,会返回 true;如果删除失败则返回 false。

1.13.5 结合 Proxy 实现观察者模式

观察者模式(Observer Mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行。

// 模拟观察者队列
const queueObservers = new Set();

// 定义 observe 函数
const observe = fn => queueObservers.add(fn);
const observable = obj => new Proxy(obj, {
    set(target, p, value, receiver) {
        const r = Reflect.set(target, p, value, receiver);
        // 遍历观察者队列中所有的函数,并执行
        queueObservers.forEach(observer => observer());
        return r;
    }
});

// 目标对象
const person = observable({
    name: '张三丰',
    age: 150,
})
// 观察者对象
function print() {
    console.log(`${person.name}, ${person.age}`)
}

observe(print);

person.age = 145;

1.14 类

大多数面向对象编程语言支持类和类继承的特性,而JavaScript不支持这些特性,只能通过其他方式来模拟的定义和类的继承。ES6 引入了class(类)的概念,新的class写法让对象原型的写法更加清晰,也更像传统的面向对象编程语言的写法。

1.14.1 类的由来

JavaScript 语言中,生成实例对象的传统方法是通过构造函数。下面是一个例子:

function Car(brand, color) {
    this.brand = brand;
    this.color = color;
}

Car.prototype.show = function () {
    console.log(this.brand + ', ' + this.color);
};

var car = new Car('BMW', 'blue');
car.show();

ES6 提供了更接近传统语言的写法,引入了 class(类)这个概念,作为对象的模板。通过 class 关键字,可以定义类。

基本上,ES6 的 class 可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用 ES6 的 class 改写,就是下面这样:

class Car {
    constructor(brand, color) {
        this.brand = brand;
        this.color = color;
    }
    show() {
        console.log(this.brand + ', ' + this.color);
    }
}
let car = new Car('BMW', 'blue');
car.show();

注意:

  1. 定义类需要使用 class 关键字来声明
  2. 类名推荐首字母大定,并类名后不能带圆括号
  3. 在类中必须声明一个叫 constructor() 函数,它是类的构造函数
  4. 如果没有声明这个函数,那么系统会默认给一个无参的构造函数,但是这个 constructor 构造方法是从父类继承过来的。
  5. 定义好类后,要想使用这个类,必须使用 new 关键字来实例化该对象。

我们在定义类时可以紧接着通过一对圆括号来调用这个表达式。

const animal = new class {
    constructor(name) {
        this.name = name;
    }

    show() {
        console.log(this.name)
    };
}('小猫');
animal.show();

这种方式有点像匿名类的方式。

1.14.2 属性访问器

访问器属性是通过关键字 get 和 set 来创建的,语法为关键字 get 和 set 后跟一个空格和相应的标识符,实际上是为某个属性定义取值和设值函数,在使用时以属性访问的方式来使用。与自由属性不同的是,访问器属性是在原型上创建的。

class Car {
    constructor(brand, color) {
        this._brand = brand;
        this.color = color;
    }
    // 只读
    get desc() {
        return `${this._brand}, ${this.color}`
    }
    // 写
    set brand(v) {
        this._brand = v;
    }
    // 读
    get brand() {
        return this._brand;
    }
}
const car = new Car('奔驰', '红色');
console.log(car.brand);
car.brand = '宝马'
console.log(car.brand);

1.14.2 静态方法

ES6 引入了关键字 static,用于定义静态方法。除构造函数外,类中所有的方法和访问器属性都可以用 static 关键字来定义。

class Car {
    constructor(brand, color) {
        this._brand = brand;
        this.color = color;
    }
    static create() {
        return new Car('法拉利', '红色')
    }
}
let c = Car.create();
console.log(c);

1.14.3 类的继承

在 ECS6 之前是不支持继承的,想要实现类的继承,需要采用一些额外手段来模似。而 ES6 提供了 extends 关键字,这样可以很轻松地实现类的继承。

class Person {
    constructor(name) {
        this.name = name;
    }

    work() {
        console.log(this.name + '正在工作...');
    }
}

class Student extends Person {
    constructor(name, no) {
        super(name); // 必须调用父类构造器
        this.no = no;
    }
}

const stu = new Student('小张', 100);
stu.work()

注意:

  1. 使用 extends 关键字来实现类的继承
  2. extends 关键字前面的类叫子类,后面的类叫父类
  3. 子类中的构造方法(constructor)中第一句必须使用 super() 来调用父类的构造方法。因为要先实例化父类再实例化子类。

9. 今天作业

把今天的内容消化。

明天上午:1班张旭

明天下午:2班张旭
console.log(this.name)
};
}(‘小猫’);
animal.show();


> 这种方式有点像匿名类的方式。

### 1.14.2 属性访问器

访问器属性是通过关键字 get 和 set 来创建的,语法为关键字 get 和 set 后跟一个空格和相应的标识符,实际上是为某个属性定义取值和设值函数,在使用时以属性访问的方式来使用。与自由属性不同的是,访问器属性是在原型上创建的。

```js
class Car {
    constructor(brand, color) {
        this._brand = brand;
        this.color = color;
    }
    // 只读
    get desc() {
        return `${this._brand}, ${this.color}`
    }
    // 写
    set brand(v) {
        this._brand = v;
    }
    // 读
    get brand() {
        return this._brand;
    }
}
const car = new Car('奔驰', '红色');
console.log(car.brand);
car.brand = '宝马'
console.log(car.brand);

1.14.2 静态方法

ES6 引入了关键字 static,用于定义静态方法。除构造函数外,类中所有的方法和访问器属性都可以用 static 关键字来定义。

class Car {
    constructor(brand, color) {
        this._brand = brand;
        this.color = color;
    }
    static create() {
        return new Car('法拉利', '红色')
    }
}
let c = Car.create();
console.log(c);

1.14.3 类的继承

在 ECS6 之前是不支持继承的,想要实现类的继承,需要采用一些额外手段来模似。而 ES6 提供了 extends 关键字,这样可以很轻松地实现类的继承。

class Person {
    constructor(name) {
        this.name = name;
    }

    work() {
        console.log(this.name + '正在工作...');
    }
}

class Student extends Person {
    constructor(name, no) {
        super(name); // 必须调用父类构造器
        this.no = no;
    }
}

const stu = new Student('小张', 100);
stu.work()

注意:

  1. 使用 extends 关键字来实现类的继承
  2. extends 关键字前面的类叫子类,后面的类叫父类
  3. 子类中的构造方法(constructor)中第一句必须使用 super() 来调用父类的构造方法。因为要先实例化父类再实例化子类。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值