Web学习笔记——ES6(下)

六、对象的扩展

1、对象字面量

对象字面量就是使用 {} 去定义对象。

属性的简洁表示法

在 ES6 之前我们可能会像下面这样来定义:

const name = "闷墩儿";
const age = 2;
const dog = { name: name, age: age };

在上面代码中,定义了一个名为 dog 的对象,其属性名与属性值的变量名相同,但在定义的对象还是要重复写两遍。
有了 ES6 之后,我们可以使用属性初始化的简写语法,消除这种属性名称与局部变量之间的重复书写。简洁表示法如下所示:

const dog = { name, age };
方法的简洁表示法

除了属性有简写的形式外,方法也有简洁的表示法。
在 ES6 之前,如果要为对象添加方法,必须通过指定名称并完整定义函数,我们可能会像下面这样来定义:

const name = "闷墩儿";
const dog = {
  run: function () {
    return name + "在公园里奔跑!";
  },
};

有了 ES6 之后,就可以不用冒号和 function 关键字了。我们可以用以下简洁表示法:

const name = "闷墩儿";
const dog = {
  run() {
    return `${name}在公园里奔跑!`;
  },
};
属性名表达式

在 ES6 之前,我们只能使用标识符的方式来作为属性名,例如:

dog.name = "闷墩儿";

而在 ES6 之后,我们还可以使用表达式来作为属性名,例如:

dog["n" + "ame"] = "闷墩儿";

我们还可以将定义的模版字面量放入 [] 中,例如:

const key = `name`;
const dog = {
  [key]: "闷墩儿",
};

定义在 [] 中的属性说明该属性是可以被计算的动态属性,其内容是可以被计算的,最后会转换成字符串,这提高了代码的灵活性。

2、对象的扩展运算符

在对象中引入扩展运算符后,我们可以用来遍历参数对象中的所有属性,并将其拷贝到新对象中。

let obj1 = { species: "柯基", name: "闷墩儿", age: 2 };
let obj2 = { ...obj1 };
console.log(obj2);

还可以使用扩展运算符将两个对象合并到一个新对象中。举个例子:

let obj1 = { species: "柯基", name: "闷墩儿", age: 2 };
let obj2 = { food: "狗粮" };
let obj3 = { ...obj1, ...obj2 };
console.log(obj3);

3、对象的新增方法

Object.is

在 ES6 之前,如果我们要判断两个值是否相等,可以用 == 或者 ===,但是这两种判断方式都存在一些缺点。

console.log(-0 == +0); // true
console.log(-0 === +0); // true

console.log(NaN == NaN); // false
console.log(NaN === NaN); // false

console.log(7 == "7"); // true

从输出的结果,我们可以看出其中的问题所在:

  • 在 JavaScript 引擎中,-0 和 +0 代表两个完全不同的实体,而使用 == 和 === 的判断结果却是相等的。
  • == 和 === 对于 NaN 的判断都是 false。
  • 使用 ==,判断整型 7 和字符串 7 的结果是 true。

基于上述这些缺点,在 ES6 中提出了同值相等的算法,就是使用 Object.is 来比较两个值是否相等。

console.log(Object.is(-0, +0)); // false
console.log(Object.is(NaN, NaN)); // true
console.log(Object.is(7 == "7")); // false
Object.assign

在 ES6 之前对象组合中我们往往会用到 mixin() 方法,其方法的作用就是一个对象接受另一个对象的属性和方法。
在 ES6 中引入了 Object.assign 来合并对象,该方法一个对象可以接受任意多个对象的属性和方法。

const obj1 = { name: "闷墩儿", food: "狗粮" };
const obj2 = { age: 2, hobby: "跑圈圈" };
const obj3 = { food: "鸡胸肉", color: "黑白黄" };
Object.assign(obj1, obj2, obj3); // 将 obj2 和 obj3 合并到 obj1 中
console.log(obj1);

被合并的对象中出现同名属性,后面的对象会覆盖前面的对象中的属性值。
该合并方式是一种浅拷贝,也就是说如果被合并对象中的属性发生变化,合并后的对象不会继承修改的属性。

let obj1 = { name: "闷墩儿", food: "狗粮" };
let obj2 = { age: 2, hobby: "跑圈圈" };
let obj3 = { color: "黑白黄" };
Object.assign(obj1, obj2, obj3); // 将 obj2 和 obj3 合并到 obj1 中
console.log(obj1);
obj2.hobby = "游泳";
console.log(obj2);
console.log(obj1);

修改 obj2 的 hobby 属性后,obj1 没有继承。

七、Set 和 Map

1、Set 对象

Set 是 ES6 提供的一种新的数据结构,其结构与数组类似,但与数组不同的是 Set 里面不允许存放相同的元素,也就是说 Set 中的每个值都是独一无二的。
Set 对象有两种创建形式:

  • 不带参数的 Set。
let s = new Set();
s.add(1);
s.add(2);
s.add(2);
s.add(3);
s.add(3);
console.log(s);

如果我们想知道 Set 中的元素个数,可以用 size 属性。在上面代码中添加:

console.log(s.size);
  • 带参数的 Set。
let s = new Set(argument1, argument1,...);
let colors = new Set(["Green", "Red", "Orange"]);
console.log(colors);

注:不能像数组那样使用索引去访问元素。

Set 相关的方法

在 Set 中使用 delete() 方法来移除指定元素。其使用格式为:

Set.delete(element);

需要注意的是,delete() 里面的参数是要删除的元素,而不是其索引。
使用 has() 方法来检查某个元素是否存在于 Set 中。
若我们想删除 Set 中的所有数据,可以使用 clear() 方法。

Set 的遍历

我们使用 forEach() 方法可以遍历 Set 中的元素。
其使用格式为:

Set.prototype.forEach(callback[,thisArg])

参数说明如下:

  • callback 是 Set 中每个元素要执行的回调函数。
  • thisArg 是回调函数执行过程中的 this 值。
let dogs = new Set(["柯基", "博美", "比熊"]);
dogs.forEach(function details(values) {
  console.log(`Hello,我是一只小${values}`);
});

在上面代码中,遍历了 dogs,在 forEach 方法里,给回调函数命名为 details,该回调函数接收一个参数 values,就是 Set 对象中的值,遍历 Set 中的值并在控制台中输出。
还可以改写成下面代码:

let dogs = new Set(["柯基", "博美", "比熊"]);
// 回调函数
function details(values) {
  console.log(`Hello,我是一只小${values}`);
}
dogs.forEach(details);
WeakSet

Set 实例和变量在存储数据方面的内存分配和垃圾回收机制是一样的。
在 JavaScript 中,当创建了一个值,分配给了相应的内存空间,值不需要了,就释放掉分配的空间,这就是垃圾回收。
如果 Set 实例中的引用一直存在,垃圾回收就不能释放该对象的存储空间,即使你并没有用到它。例如:

let s = new Set();
let obj1 = {};
let obj2 = {};
s.add(obj1);
s.add(obj2);
console.log(s.size); // 2
obj1 = null;
console.log(s.size); // 2

上面代码中,先声明了一个空的 Set 对象 s,然后调用 add 方法向 s 中添加两个空对象元素,控制台打印 s 的元素个数为 2,证明 Set 对象中给空对象也分配了内存;接着把对象 obj1 设为 null,再次打印 s 的元素个数,仍然为 2,证明 Set 实例 s 中元素 1 占用的内存并没有被释放掉。
针对这个缺陷,ES6 又给我们提供了另一种 Set,叫做 WeakSet。
WeakSet 也叫做弱引用 Set,如果将其存储的对象设为了 null,相当于是删除了该对象,当垃圾回收机运行时,会释放掉被删除对象占用的空间。

let s = new WeakSet();
let obj1 = {};
let obj2 = {};
s.add(obj1);
s.add(obj2);
console.log(s.size);
obj1 = null;
console.log(s.size);

Set 与 WeakSet 的区别:

  • WeakSet 的成员只能是对象且都是弱引用。

  • 在 WeakSet 中,add() 方法中不能传入非对象参数,若传入会报错。
    (在 WeakSet 中,给 has() 和 delete() 方法传入非对象参数,虽然不会报错,但是会返回 false。)

  • WeakSet 对象没有 size 属性,不能被遍历。
    (由于 WeakSet 里面存储的都是弱引用,内部有多少个成员,取决于垃圾回收机制有没有运行。运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历。)

2、Map 对象

在 ES6 之前,对象是创建键值对数据结构的主要方式,但对象在使用上有一些局限性。

  • 在对象中的键名只能是字符串、数字或者 Symbol。
  • 对象不可以直接使用 forEach 方法遍历。

而 Map 的出现就解决了上述问题。
Map 是 ES6 中一种存储许多键值对的有序列表,其键值对可以是任意数据类型。Map 是有序的,它会按照键值插入的顺序来排列。
创建 Map 对象的语法格式为:

let map = new Map([iterable]);

Map 对象可以接收一个由键值对组成的可叠对象。
set 方法添加数据
使用 set() 方法可以向对象中添加数据。其使用格式为:

map.set(key:value);
let bookstore = new Map();
bookstore.set([1, 2, 3], "书籍");
bookstore.set(false, "日用品");
bookstore.set(3, "化妆品");
console.log(bookstore);
  • 在 bookstore.set([1,2,3],“书籍”) 中创建了数组类型的键值 [1,2,3],其值为普通字符串“书籍”。
  • 在 bookstore.set(false,“日用品”) 中创建了布尔类型的键值 false,其值为普通字符串“日用品”。
  • 在 bookstore.set(3,“test”); 中创建了整数类型的键值 3,其值为普通的字符串“化妆品”。

get 方法从集合中获取数据
我们要获取集合中的数据,使用 get() 方法即可。
其使用格式为:

map.get(key);

我们来获取一下 bookstore 集合中的数据吧~

console.log(bookstore.get(false));

其他常用方法
除了上方提到的 set() 和 get() 方法,在 Map 中,还有下面三种方法比较常用。

  • has() 用来判断指定键名对应的数据是否存在于当前集合中。
  • delete() 用来删除指定键名的数据。
  • clear() 用来清空集合中的数据。
Map 的遍历

与对象不同,Map 可以使用 forEach() 方法来遍历数据值。
创建并初始化一个带数据的 Map。
语法格式如下:

let map = new Map([[key1,value1],[key2,value2]...]);
let userName = new Map([
  [1, "小红"],
  [2, "小蓝"],
  [3, "小白"],
]);
console.log(userName);

Map 有一个 forEach() 方法,与数组的 forEach() 方法类似,可以实现对 Map 实例的遍历。

map.forEach(callback(value, key, ownerMap));

callback 是一个回调函数,其函数包含三个参数:

  • value:本次循环所读取的元素的数据。
  • key:本次循环所读取的元素的键名。
  • ownerMap:Map 集合本身。
let userName = new Map([
  [1, "小红"],
  [2, "小蓝"],
  [3, "小白"],
]);
userName.forEach(function (value, key, ownerMap) {
  console.log(`userName 中的值有: ${value}`);
  console.log(`userName 中的键有:${key}`);
  console.log(ownerMap);
});

八、异步编程

1、Promise 对象基础应用

地狱式回调
var outnum = function (n, callback) {
  setTimeout(function () {
    console.log(n);
    callback();
  }, 1000);
};
outnum("1", function () {
  outnum("2", function () {
    outnum("3", function () {
      console.log("0");
    });
  });
});

在上述代码中我们发现,虽然可以通过回调函数层层嵌套的形式达到最终数据请求的目的,但代码结构不甚明朗,可读性极差,这就是传说中的回调地狱。

定义 Promise 对象

为了解决这种地狱式的回调,可以使用 Promise 对象,且代码更优雅,由于 Promise 对象是一个构造函数,因此,必须通过实例化来生成,它的定义格式如下代码:

let p = new Promise(function (resolve, reject) {
  // 此处做一个异步的事情
});

在定义格式的代码中,需要说明的几个问题:

  • 在实例化中,参数为函数,函数中又有两个用于回调的函数。
  • 两个回调函数中,resolve 为异步执行成功时的回调,其参数可以传递执行的结果。
  • reject 为异步执行失败时的回调,其参数可以传递失败的错误信息。
Promise 对象的 then 方法

Promise 对象实例化后,可以调用 then 方法获取两个回调函数中的传参值,该方法接收两个回调函数作为参数,第一个参数是必选参数,表示异步成功后执行的 resolve 回调函数,第二个参数是可选参数,表示异步失败后执行的 reject 回调函数,它的调用格式如下:

//回调函数不带参数
p.then(
  function () {},
  function () {}
);
//回调函数带参数
p.then(
  function (v) {},
  function (e) {}
);

其中参数 v 值表示 resolve 回调函数中的参数值,e 值表示 reject 回调函数中的参数值,如下列代码所示:

let n = 6;
let p2 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    if (n > 5) {
      resolve(n);
    } else {
      reject("必须大于5");
    }
  });
});
p2.then(
  function (v) {
    console.log(v);
  },
  function (e) {
    console.log(e);
  }
);
// 执行代码后,由于 n 值大于 5 ,因此,在控制台中输出数字 6 。

此外,一个 then 方法被执行后,如果仍然返回一个 Promise 对象,则可以继续再执行 then 方法,形成链式写法效果,代码如下所示:

p1.then(function (v) {
  return p1;
}).then(function (v) {
  return p1;
});
解决地狱式回调
var outnum = function (order) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      console.log(order);
      resolve();
    }, 1000);
  });
};
outnum("1")
  .then(function () {
    return outnum("2");
  })
  .then(function () {
    return outnum("3");
  })
  .then(function () {
    console.log("0");
  });

执行上述代码之后的页面效果与使用地狱式回调方式是完全一样的,但 Promise 对象实现的代码可读性更强,还可以很方便地取到异步执行后传递的参数值,因此,这种代码的实现方式,更适合在异步编程中使用。

2、Promise 对象中的方法

Promise.all 方法

当首次加载某个页面时,由于数据庞大需要分别同时发送多个异步请求向服务器获取数据,最终所有数据返回之后做下一步操作(如“隐藏页面的加载 loading 动画”)。由于很难捕获这些异步请求全部成功的时机,导致这个需求实现起来相当困难。这时使用 Promise.all 方法就可以解决这种问题。
使用格式
Promise.all 方法中的参数是一个数组,数组中的每个元素是实例化后的 Promise 对象,格式如下代码:

Promise.all([p1,p2,p3,...]).then(res=>{
  // 所有请求成功后的操作步骤
},error=>{
  // 某一个请求失败后的操作步骤
});

上述代码中,p1、p2、p3 都是实例化后的 Promise 对象,并且该方法可以通过链式写法直接调用 Promise.all 中的 then 方法,当全部的实例化对象都执行成功后,进入 then 方法的第一个执行成功的回调函数中,函数参数是每个任务执行成功后的结果,以数组形式保存,如下图所示:
在这里插入图片描述
如果在调用 Promise.all 方法时,有一个 Promise 实例对象(比如:p1)的任务执行失败了,则会直接进入 Promise.all 后的 then 方法的失败回调函数中,如下图所示:
在这里插入图片描述
通过 Promise.all 方法可以并列完成多个异步的请求,只有当全部请求成功后,才进入 then 方法中的成功回调函数中,否则,进入失败的回调函数中,因此,当首次加载页面时,可以将各种的异步请求放入 Promise.all 方法中,如果全部完成,则在 then 方法中的成功回调函数中执行下步操作,否则,直接进入失败回调函数中。

function p1(n) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      if (n > 0) {
        resolve(n);
      } else {
        reject("不能小于0");
      }
    }, 1000);
  });
}

Promise.all([p1(5), p1(6), p1(7)]).then( //三个执行成功的任务
  function (v) {
    console.log(v);
  },
  function (e) {
    console.log(e);
  }
);

/*Promise.all([p1(5), p1(-2), p1(7)]).then( //一个执行失败的任务,二个执行成功的任务
  function (v) {
    console.log(v);
  },
  function (e) {
    console.log(e);
  }
);*/
Promise.race 方法

与 Promise.all 方法不同,Promise.race 方法是多个 Promise 实例化对象在比赛, 执行最快的那个任务的结果,将返回给 then 方法中的对应回调函数中,通过这种方式,可以检测页面中某个请求是否超时,并输出相关的提示信息。
使用格式
与 Promise.all 方法一样,Promise.race 中的参数也是一个数组,每个元素也是实例化后的 Promise 对象,格式如下代码:

Promise.race([p1,p2,p3,...]).then(
    function(v){
      //获取最快任务成功时的返回值
  },
  function(){
      //获取最快任务失败时的返回值
  }
)

例子:

function timeOut() {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      reject("请求超时");
    }, 5000);
  });
}

function loadData() {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve("请求成功");
    }, 3000);
  });
}

Promise.race([loadData(), timeOut()]).then(
  function (d) {
    console.log(d);
  },
  function (e) {
    console.log(e);
  }
);

由于 loadData 函数的延时时间小于请求超时的延时时间,因此,该任务执行最快,所以在控制台显示 ”请求成功“ 的信息。

3、Promise.then 方法的缺点

Promise 对象虽然很优雅地解决了地狱回调的情形,使代码更简洁和易读,但通过 then 方法取值时,代码还是不够时尚和前沿,多层嵌套取值时也不够高效,如下列代码所示:

var p = function (msg) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(msg);
    }, 1000);
  });
};
p("明明")
  .then(function (v) {
    return p(v + ",男");
  })
  .then(function (v) {
    return p(v + ",今年18岁");
  })
  .then(function (v) {
    console.log(v);
  });

在上述代码中,then 方法在取值和传值时,如果层级多时,它的代码的结构并不易读。

4、async 关键字和 await 关键字

async 关键字

async 英文单词的意思是异步,虽然它是 ES7 中新增加的一个关键字,但它的本质是一种语法糖写法(语法糖是一种简化后的代码写化,它能方便程序员的代码开发),async 通常写在一个函数的前面,表示这是一个异步请求的函数,将返回一个 Promise 对象,并可以通过 then 方法取到函数中的返回值。

async function fn() {
  return "12345";
}
fn().then((val) => {
  console.log(val);
});

在上述代码中,定义一个名称为 fn 的函数,但由于在函数前添加了关键字 async ,使这个函数将返回一个 Promise 对象,因此,函数执行后,可以直接调用 then 方法;同时,fn 函数中的返回值,就是 then 方法中,执行成功回调函数时的参数值,因此,执行上述代码后,将在页面的控制台输出 “12345” 字符。

  • 使用 async 关键字定义的函数,将会返回一个 Promise 对象。
  • 函数中有返回值,则相当于执行了 Promise.resolve(返回值) 函数,没有返回值,则相当于执行了 Promise.resolve() 函数。

虽然 async 关键字简化了我们之前实现异步请求中返回 Promise 实例对象的那一步,直接返回了一个 Promise 对象,但是仍然需要在 then 方法中处理异步获取到的数据。

await 关键字

await 可以理解为 async wait 的简写,表示等待异步执行完成,await 必须在 async 定义的函数中,不能单独使用,await 后可以返回任意的表达式,如果是正常内容,则直接执行,如果是异步请求,必须等待请求完成后,才会执行下面的代码。

// 函数 p 返回的是一个 Promise 对象,在对象中,延时 2 秒,执行成功回调函数,相当于模拟一次异步请求
function p(v) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      // 在 p 函数执行时,将函数的实参值 v ,作为执行成功回调函数的返回值。
      resolve(v);
    }, 2000);
  });
}

// 一个用于正常输出内容的函数
function log() {
  console.log("2.正在操作");
}

async function fn() {
  console.log("1.开始");
  await log();
  let p1 = await p("3.异步请求");
  console.log(p1);
  console.log("4.结束");
}
fn();
  • fn 函数执行后,首先,会按照代码执行流程,先输出“1.开始”。
  • 其次,对于没有异步请求的内容,在 await 后面都将会正常输出,因此,再输出“2.正在操作”。
  • 如果 await 后面是异步请求,那么,必须等待请求完成并获取结果后,才会向下执行。
  • 根据上述分析,由于 方法 p 是一个异步请求,因此,必须等待它执行完成后,并将返回值赋给变量 p1,再执行向下代码。
  • 所以,最后的执行顺序是,先输出 “3.异步请求”,再输出 “4.结束”。

在 async 函数中的执行顺序,如下图所示。
在这里插入图片描述

多层嵌套传参数的优化

基于 await 的特性,可以将异步请求的代码变成同步请求时的书写格式,代码会更加优雅,特别是处理多层需要嵌套传参时,使用 await 的方式,代码会更少,更易于阅读。

// 函数 p 返回的是一个 Promise 对象,在对象中,延时 2 秒,执行成功回调函数,相当于模拟一次异步请求
function p(v) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      // 在 p 函数执行时,将函数的实参值 v ,作为执行成功回调函数的返回值。
      resolve(v);
    }, 2000);
  });
}
async function fn() {
  let p1 = await p("1");
  let p2 = await p(p1 + "2");
  let p3 = await p(p2 + "3");
  console.log(p3);
  console.log("登录成功!");
}
fn();
  • 在 fn 函数中,第一次发送请求时,返回值为 “1”,并保存在变量 p1 中;
  • 然后,将变量 p1 作为参数,并加 “2” 发送第二次请求,返回值为“12”,并保存在变量 p2 中;
  • 然后,将变量 p2 值作为参数,并加 ”3“ 发送第三次请求,返回值为 ”123“,并保存在变量 p3 中;
  • 最后,在控制台输出的内容是 p3 的值,即字符 “123”,同时,输出 “登录成功!”的字样。
多个并列异步请求的调优

await 在处理多个异步请求时,如果请求之间有嵌套关系,可以一次一次按顺序发送请求,但是如果各个请求之间无任何关联,则可以将这些请求借助 Promise.all 一次性并列发送,使用 await 关键字获取请求的结果,并根据返回的结果进行下一步操作。

// 函数 p 返回的是一个 Promise 对象,在对象中,延时 2 秒,执行成功回调函数,相当于模拟一次异步请求
function p(v) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      // 在 p 函数执行时,将函数的实参值 v ,作为执行成功回调函数的返回值。
      resolve(v);
    }, 2000);
  });
}
async function fn() {
  await Promise.all([p("a"), p("b"), p("c")]);
  console.log("隐藏加载动画!");
}
fn();

在上述实现的代码中,方法 Promise.all 中每个实例化的 Promise 对象,都会以并行的方式发送异步请求,当所有请求都成功后,才会去执行输出字符内容的代码。
修改后代码:

async function fn() {
  await p("a");
  await p("b");
  await p("c");
  console.log("隐藏加载动画!");
}
fn2();

无论是函数修改之前还是修改之后 ,都使用了 async 和 await,并且两次的执行结果都是一样的,但在 fn 函数修改之前,所有的异步请求都是并行发送的,而在函数修改之后,所有的异步请求都是按顺序执行的。
从性能上来看,fn 函数修改之前的异步请求并发执行明显高于修改之后的阻塞式异步请求,因此,虽然学习了 async 和 await ,但也不能完全取代 Promise 对象,需要结合实际需求场景去使用。

九、模块化

模块化开发项目是目前的主流趋势,它是将复杂的功能分解成各自独立的子模块,所有子模块按照一种方式进行组合,最终完成复杂功能的过程,它的优势是各模块是独立工作的,更利于复用和维护,同时更有利于节略资源,提高效率。

1、export(导出)

在这里插入图片描述
关键字 export 可以将一个模块中的方法、变量和其他功能从模块中输出,允许其他需要的模块按指定的标准进行访问,没有使用关键字 export 输出的模块内容,是封闭的,其它模块无法访问到它,下面是关键字 export 几种输出的方式 。

  • 可以直接输出一个模块文件中的变量,如下代码:
export let name = "小蓝同学";
export let age = 18;
let work = "一直在写代码呢!";

在上述代码中,由于变量 name 和 age 之前都使用了输出关键字 export ,因此,它们都可以被其他模块访问,由于变量 work 之前没有添加关键字 export ,所以,其他的模块无法访问到这个变量。
上述代码的这种写法,还可以合并成一个对象,并使用关键字 export 一次性输出,修改后的代码如下:

let name = "小蓝同学";
let age = 18;
let work = "一直在写代码呢!";
export { name, age };

修改后的这种方法更加有利于一次性地批量输出多项内容。
关键字 export 除能输出模块中的变量外,还可以输出模块中的方法,两者的输出格式都是相同的,如下代码:

function say() {
  console.log("我的名字叫小蓝");
}
function act() {
  console.log("我的工作是写代码");
}
export function log() {
  console.log("说点什么。。。");
}
export { say, act };

关键字 export 在输出方法时,不用添加执行方法的括号,只要输出方法的名称就可以,否则就会报错;此外,在输出内容的过程中,还可以使用关键字 as 来修改输出后的名称,修改后的代码如下:

export {
    say(), //报错
    act as userWork
}

在上述代码中,由于在输出 say 方法时,添加了括号,表示执行该方法,因此,代码报错;同时,在输出 act 方法时,添加了关键字 as ,它的作用是给输出的功能取一个别名,因此,其他模块在使用输出的 act 方法时,只能访问它的别名 userWork 。

2、import(导入)

在这里插入图片描述
关键字 import 在输入模块中加载输出模块的变量时,可以使用大括号包裹全部变量名,各个变量之间使用逗号分割,再通过 from 指定输出模块的路径,这个路径可以是绝对的,也可以是相对的,代码格式如下:

import { 变量1,变量2,变量3...} from 输出模块位置

在上述格式代码中,大括号中的变量 1,变量 2 也可以通过关键字 as 取一个别名,格式如下:

import { 变量1 as a1,变量2 as fn,变量3...} from 输出模块位置

取了别名之后,在输入模块中,只能使用这个别名,而不能再使用原先的名称,否则,将会出现变量未定义的错误信息。
导出代码(outfile.js):

let strSex = "女";
let strName = "小蓝";
let strLike = "就喜欢写点代码";

function log(v) {
    console.log(v);
}

export { strSex, strName, strLike, log };

导入代码(index.html):

<script type="module">
    import { strName, strSex, strLike, log } from "./outfile.js";
    console.log(strName + "," + strSex + "," + strLike);
    log("写代码,是一种情怀");
</script>

3、数据排序的模块开发

在这里插入图片描述

  • 数据模块(data.js):
let _data = [12, 18, 16, 20];
export { _data as data };

在数据模块代码中,定义一个名称为 _data 的数组,并使用关键字 export 将该数组输出,用于逻辑模块的使用

  • 逻辑模块(logic.js):
import { data } from "./data.js";
function sort(blnS) {
  if (blnS)
    return data.sort(function (a, b) {
      return a - b;
    });
  else
    return data.sort(function (a, b) {
      return b - a;
    });
}
export { sort };

在逻辑模块代码中,先使用 import 输入数据模块中的数组,再定义一个 sort 函数,对加载的数组进行逻辑操作,通过布尔值 blnS 控制排序的方向,最后,再使用关键字 export 将该函数输出,用于视图模块的使用。

  • 视图模块(index.html):
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script type="module">
      import { sort } from "./logic.js";
      console.log("升序: " + sort(true));
      console.log("降序: " + sort(false));
    </script>
  </body>
</html>

十、Proxy(代理)

1、Proxy介绍

Proxy 可以对目标对象的读取、函数调用等操作进行拦截,然后通过对象的代理对象进行操作。也可以理解为在外界与对象之间建立了一道门,外界要访问该对象必须先打开这道门,如果想要获得打开该门的钥匙,就要遵守一个访问“条约”,允许对来访人员进行改造(提供一种机制:可以对外界的访问进行过滤和改写)。
用 Proxy 创建代理需要传入两个参数:目标对象(target)和处理程序(handler)。语法格式如下:

var proxy = new Proxy(target, handler);

参数说明如下:

  • target:要拦截的目标对象。
  • handler:制定拦截行为的对象。
let target = {};
let proxy = new Proxy(target, {});
proxy.name = "闷墩儿";
console.log(proxy.name);
console.log(target.name);

target.name = "憨憨";
console.log(proxy.name);
console.log(target.name);

在上面的例子中 proxy 的所有操作都转发给了 target 对象,当在 proxy 上创建了 name 属性之后,相应的 target 上也会创建 name 属性。由于 target.name 和 proxy.name 都是引用的 target.name,所以当修改了 target.name 的值后,proxy.name 的值也会修改。

2、Proxy 的实例方法

Proxy 的代理拦截方法如下表所示:

拦截方法方法说明
get(target, propKey, receiver)拦截对象属性的读取。
set(target, propKey, value, receiver)拦截对象属性的设置。
has(target, propKey)拦截 propKey in proxy 的操作。
deleteProperty(target, propKey)拦截 delete proxy[propKey]的操作。
ownKeys(target)拦截 Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in 循环,返回一个数组。
getOwnPropertyDescriptor(target, propKey)拦截 Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
defineProperty(target, propKey, propDesc)拦截 Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
preventExtensions(target)拦截 Object.preventExtensions(proxy),返回一个布尔值。
getPrototypeOf(target)拦截 Object.getPrototypeOf(proxy),返回一个对象。
isExtensible(target)拦截 Object.isExtensible(proxy),返回一个布尔值。
setPrototypeOf(target, proto)拦截 Object.setPrototypeOf(proxy, proto),返回一个布尔值。
apply(target, object, args)拦截 Proxy 实例作为函数调用的操作。
construct(target, args)拦截 Proxy 实例作为构造函数调用的操作。
get(target, propKey, receiver)

在 JavaScript 中,当我们去访问一个对象中不存在的属性时,不会报错,而是返回一个 undefined。如下例所示:

let dog = {};
console.log(dog.name);

这样的模式在大型的代码库中可能会导致严重的问题。ES6 中为我们提供了 get 方法,在访问对象之前检验一下是否存在你要访问的属性,该方法接受三个参数,具体说明如下:

  • target:被读取属性的目标对象。
  • propKey:要读取的属性键值。
  • receiver:操作发生的对象。
let dog = {
  name: "闷墩儿",
};
var proxy = new Proxy(dog, {
  get(target, propKey) {
    // 遍历目标对象的属性键值
    if (propKey in target) {
      return target[propKey]; // 返回相应的属性值
    } else {
      throw new ReferenceError(propKey + " 属性不存在");
    }
  },
});
console.log("访问 dog 对象中的 name 属性值为:" + proxy.name);
console.log("访问不存在的 age 属性:" + proxy.age);

在控制台可以看见访问 age 属性就报错了。

set(target, propKey, value, receiver)

如果要创建一个只接受数字作为属性值的对象,那么在创建属性时,必须判断该值是否是数字,若不是数字应该报错。我们使用 set 方法就可以实现这个需求。
set 方法接受四个参数,具体说明如下:

  • target:用于接收属性的目标对象。
  • propKey:要写入的属性键值。
  • value:要写入的属性值。
  • receiver:操作发生的对象。
let validator = {
  set(target, propKey, value) {
    if (propKey === "age") {
      // 判断 age 属性值是否时数字
      if (!Number.isInteger(value)) {
        throw new TypeError("狗狗的年龄只能是整型哦!");
      }
    }
    target[prop] = value;
    return true;
  },
};

let dog = new Proxy({}, validator);
console.log((dog.age = "22"));

从控制台的输出结果可以看出,当 age 属性值设为字符串时,抛出错误。

has(target, propKey)

在 ES6 之前如果我们要判断某个属性是否在该对象中,可以使用 in 来判断。例如:

let dog = {
  name: "闷墩儿",
};
console.log("name" in dog);
console.log("valueOf" in dog);

在控制台却会看到两个都输出了 true。因为 valueOf 是一个继承自 object 的原型属性。
在 has 方法中可以拦截这些操作,返回不一样的值。has 方法接收两个参数,具体说明如下:

  • target:读取属性的目标对象。
  • propKey:要检查的属性键值。
let dog = {
  name: "闷墩儿",
  age: 2,
};
let handler = {
  has(target, propKey) {
    if (propKey == "age" && target[propKey] < 5) {
      console.log(`${target.name}的年龄小于 5 岁哦!`);
      return true;
    }
  },
};
let proxy = new Proxy(dog, handler);

console.log("age" in proxy);
ownKeys(target)

ownKeys 方法用于拦截对象自身属性的读取操作,具体可以拦截以下四种操作:

  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.keys()
  • for…in
let dog = {
  name: "闷墩儿",
  age: 2,
  food: "狗罐头",
};
const proxy = new Proxy(dog, {
  ownKeys() {
    return ["name", "color"];
  },
});

for (let key in proxy) {
  console.log(key); // 输出 name
}

从控制台结果可以看到只输出了 name 属性,这是因为在 dog 对象中不包含 color 属性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值