怎样实现良好的数据库设计?

thbcm阅读(278)

1. 为什么要关注数据库设计?

无论是应用程序,还是数据库如何变化,数据始终是最重要的部分。通常,数据是系统存在的首要目的。这就是为什么,我们不应该只把数据库系统看作是保存数据的黑盒子,而要将其看成验证和防止数据腐化的工具。

要做到这一点,就要有健壮和深思熟虑的数据库设计。当然,业务逻辑是在应用层编码,它确保数据在到达数据库之前的格式是正确的。

但是,谁能保证网络故障或缺陷不会放行不可靠的“客人”?此外,应用层并不是通往数据库的唯一的“门”。我们可以使用导入脚本、维护脚本,DBA 和开发人员也会与之交互。我们可以在底层采取预防措施确保在数据存储前总是进行检查。

拥有健壮、可靠的数据也有助于开发和测试。将一个列设置为 Not Null 可以省掉许多假设该列为空的测试场景,还能简化代码,让开发人员不用(几乎)每次访问它之前都检查值。

在强调了良好的数据库设计的重要性后,让我们看看可以使用哪些工具来实现它。

2. 规范化

这无疑是良好设计的首要原则。这里,我们不打算深入研究规范化规则,只是想强调它的重要性。

关于这个话题,这里有份不错的资料,你可以进一步阅读。

https://docs.microsoft.com/en-us/office/troubleshoot/access/database-normalization-description

3. 数据类型

另一件要注意的事情是定义适当的属性类型。这不仅可以提高数据库的性能,还能在存储数据前验证数据。所以,我们应该在“integer”、“numeric”字段中保存数值数据;在“timestamp”、“timestamptz”字段中保存时间戳;在“bit”、“char(1)”或“boolean”字段中保存布尔值等等。

日期值得特别注意。如果 Date 属性假设只有日期部分(OrderDate,ReleaseDate),请使用没有时间部分的 Date 类型。如果你只需要保留时间(StartTime,EndTime),就使用合适的时间类型。

如果不需要指定精度,则将其指定为零(“time(0)”)。对带有时间部分的日期,有一个问题是,你必须总是截断时间部分,只显示日期,并且当你要在与数据库所在时区不同的地方显示时,要确保格式化后不会显示成昨天或明天。当跳转到夏令时的时候,带有时间部分的日期时间加减也可能出现问题。

4. 约束

约束是本文讨论的重点。它们将无效数据排除在外,并确保数据的健壮性。让我们一个一个来看。

非空约束

如果业务规则要求该属性应该始终存在,那么要毫不犹豫地将其设置为 Not Null。适合设置为 Not Null 的字段有 Id、Name、AddedDate、IsActive、State、CategoryId(如果所有项都应该有一个类别)、ItemCount、Price 以及许多其他字段。通常,这些属性在业务逻辑中扮演重要角色。其他可选的信息字段可能还是可以设置为 Null。

但是要注意,不要对可以为空的属性使用 Not Null 约束。例如,一个长时间运行的任务总有一个 StartTimestamp(Not Null),但是只有在任务完成时才更新 EndTimestamp(Null)。

另一个典型的例子是,Employee 表的 ManagerId,并不是所有员工都有经理。不要试图让 ManagerId 不为空,并为没有经理的员工插入“0”或“-1”。当我们添加外键约束时,这将导致其他问题。

唯一约束

同样,根据业务规则,一些属性(或属性的组合)应该是惟一的,比如 Id、PinNumber、BookId 和 AuthorId、OrderNo 等。应该通过添加惟一约束来保证这些属性的惟一。

还有一点要注意:可以使用唯一索引来实现同样的效果,但是添加约束是更好的方法。因为当添加惟一约束时,会自动创建非惟一索引。

因此,如果出于某种原因,你必须临时禁用 / 启用约束,将会非常容易。在使用唯一索引的情况下,你必须删除 / 重新创建索引,从性能和时间方面来说,这是一个昂贵的操作。

主键

Not Null 和唯一约束一起构成主键。当我们想到主键时,会很快想到 Id 或 ObjectId 之类的列。但是主键也可以是复合的,比如 BookId 和 AuthorId。

这里有个难题是,是使用单独的 Id 列作为主键,还是将两者的组合作为主键?通常,使用单独的 Id 列是一种更好的方法,因为它可以使连接更加清晰,还能方便地将另一列添加到惟一组合中。但是,即使有了一个单独的主键(Id),我们还是要为 BookId 和 AuthorId 列添加唯一约束。

Check 约束

Check 约束允许我们定义数据的有效值 / 范围。适合 Check 约束的属性有百分比(0 到 100 之间)、状态(0、1、2)、价格、金额、总数(大于或等于 0)、PinNumber(固定长度)等。

同样,不要尝试将业务逻辑编码到 Check 约束中。我记得有一次,在 AccountBalance 列中添加了一个“大于或等于零”的 Check 约束,从而避免了意外透支。

默认约束

默认约束也很重要。它们允许我们向现有表中添加新的 Not Null 列,并使“旧”API 与新结构兼容,直到所有各方都完成升级(尽管在完全升级后,默认约束应该删除)。

这里要记住一点。不要在默认约束中编写业务逻辑。例如,函数“now()”可能很适合(尽管不总是)作为日志表中的时间戳字段的默认值,但不适合 Orders 表的 OrderDate 字段。你可能会倾向于在插入语句中省略 OrderDate,而依赖于默认约束,但这意味着将业务逻辑扩展到数据库层。

此外,在某些情况下,业务可能只在订单批准后才给 OrderDate 赋值,因为默认约束深埋在数据库中,所以,当我们对应用层的代码进行更改时,它不会那么明显。

外键约束

外键约束是关系数据库设计之王。外键与主键一起确保表之间的数据一致性。规范化规则告诉我们何时将数据提取到表中并使用外键引用它。这里我们将关注细节差别,比如 OnDelete 和 OnUpdate 规则。

让我们从简单的部分开始:OnUpdate。外键引用主键,它们很少(如果有的话)被修改。因此,OnUpdate 规则不是很常用,但将其设置为 Cascade 还是有意义的,因为我们有时可能必须更新某些行的主键(通常在迁移后)。这样,数据库将允许我们进行更新,并将新的 id 传播到子表中。

OnDelete 规则有点复杂。根据数据库的不同,我们有 NoAction、Restrict、SetNull、SetDefault 和 Cascade 选项。那么,选择哪一个呢?

通常,对于键引用查找或不引用实体的实体,我们选择 NoAction。例如,Products -> Categories、Books -> Authors 等。在大多数情况下,Restrict 与 NoAction 是相同的,但是对于某些数据库,它们有细微的区别。

https://www.vertabelo.com/blog/on-delete-restrict-vs-on-delete-no-action/

另一方面,当子记录不能在没有父记录的情况下存在时,选择 Cascade。在 Book 和 Author 示例中,当删除一本书时,我们也应该从 BookAuthor 表中删除记录。其他例子有 OrderDetails -> Orders、PostComments -> Posts 等。这里,有些人可能会不同意,数据库不应该自动删除子行,它们应该由应用层删除。根据业务逻辑,是这样的。但有时“不重要的”子项删除可以委托给数据库。

SetNull 很少使用。例如,Employee.ManagerId 和 Employee.Id 之间的外键可以是 SetNull。当一名经理被撤职,他的下属就没经理了。显然,只有当列可为空时才能选择该项规则。

在这些规则中,SetDefault 最罕见。当父记录被删除时,它将列设置为其默认值。因为外键引用主键,我们很难想象一个有外键的字段将默认值硬编码。但无论如何,这个选项是存在的,我们还是有可能需要它。

5. 索引

索引是良好数据库设计的重要组成部分,但有点偏离我们的讨论,因为它们几乎不能保护我们的数据(惟一索引除外)。

需要注意的一点是:一些 RDBMS 系统(例如 Oracle)会在创建外键时自动创建索引,而无需我们操心。其他数据库(例如 MS SQL Server)不会这样做,我们必须自己添加索引。

6. 小结

一个深思熟虑的设计可以为我们节省大量的编码、测试和故障排除时间。在设计良好的数据库上编写查询和报表令人愉快。将数据发布并迁移到新系统也会非常容易。

以上就是W3Cschool编程狮关于怎样实现良好的数据库设计?的相关介绍了,希望对大家有所帮助。

2020 ES6面试题你知道多少?

thbcm阅读(196)

前言

在面试的时候,ES6几乎已成必问问题,对ES6还摸棱两可的小伙伴们看完这篇文章将不再迷茫,建议收藏。

1.let 变量声明以及特性

声明变量

 let a;
 let b, c, d;
 let e = 1;
 let f = 2, g = 3; 

特性

  • 不能重复声明
  • 块级作用域 只在块级作用域有效
  • 没有变量提升
  • 不影响作用域链

2.const 常量声明以及特性

特性

  • 必须有初始值
  • 一般常量使用大写
  • 常量的值不能修改
  • 也是块级作用域
  • 对于数组和对象的修改,不算对常量的修改,不会报错(可以对数组和对象进行修改,指向的地址没有改变)

3.变量的解构赋值

数组的解构

const RNG = ['uzi', 'mlxg', 'letme', 'ming']
let [a, b, c, d] = RNG;// 每个值就是数组的位置对应的值

对象的解构

const UZI= {
  name: '自豪',
  age: 22,
  love: 'LOL',
  lol: function(){
    alert('打lol')
  }
}
let {name, age, love} = UZI;// 每个值是对象中对应的值
let {lol} = UZI;
lol();// 使用

4.模板字符串 “

内容中可以直接出现换行符,单引号双引号内容不能直接用换行符(需要通过引号加号拼接)

let str = `lol s10
      tes 是冠军`;// 可以这样直接使用换行符

直接进行变量的拼接  ${}

let lol = 'lol十周年';
let uzi = `${lol} 中国队夺冠`

5.对象的简写

let name = 'zhangning187';


let change = function(){
  console.log('努力改变');
}


const supwisdom = {
  name,
  change
}

6.箭头函数及声明特点  =>

let fn = (a, b) => {
  return a + b;
}
fn(1, 2);// 3

特点

  • 1.this是静态的,this始终是指向函数声明时所在作用域下的this值,他没有自己的this
  • 2.不能作为构造实例化对象
  • 3.不能使用arguments变量
  • 4.箭头函数的简写

省略小括号,当形参有且只有一个的时候可以省略

let add = n => {
        return n*2;
      }

省略花括号,当代码体只有一条语句的时候,return 必须省略,语句的执行结果就是函数的返回值

let add = n => n*2;

7.函数参数的默认值

形参初始值

function add(a, b, c = 10) {// 当不传递c的时候,c的默认值是10,尽量把具有默认值的参数放在后面
  return a + b + c;
}
add (1, 2);// 13

与解构赋值结合

function con({name, age, love, height = '18'}) {// 还可以给默认值
  console.log(age);// 24
  console.log(height);// 没有传递,使用默认值 18
}
con({
  name: 'zhangning187',
  age: 24,
  love: 'js'
})

8.rest参数

用于获取参数的实参,用于代替arguments

rest参数是一个数组和es5中的arguments不一样,arguments里面是一个对象

获取实参的方式

function data(a, b, ...args) {// rest 参数必须放到参数的最后面
    console.log(a);// 1
    console.log(b);// 2
    console.log(args);// [3, 4, 5, 6, 7]
}
data(1, 2, 3, 4, 5, 6, 7);

9.扩展运算符  …

可以将数组转换为逗号分隔的参数序列,将一个数组分割,并将各个项作为分离的参数序列传递给函数

const RNG = ['UZI', 'MLXG', 'LETME', 'MING'];
console.log(...RNG)// UZI MLXG LETME MING  解构之后的序列
console.log(RNG);// ['UZI', 'MLXG', 'LETME', 'MING']  返回的是一个数组
const a = [1,2], b=[3,6];
const c = [...a, ...b];// [1, 2, 3, 6]

10.Symbol  新的原始数据类型,表示独一无二的值

他是javaScript 语言的第七种数据类型,是一种类似于字符串的数据类型

特点

  • 1.Symbol 的值是唯一的,用来解决命名冲突的问题
  • 2.Symbol 值不能与其他数据进行运算,也不能自己运算 + – * /
  • 3.Symbol 定义的对象属性不能使用for in循环遍历,但是可以使用 Reflect.ownKeys 来获取对象的所有键名
//创建Symbol
let s = Symbol();// s不可见,在内部实现唯一性
let s2 = Symbol('zhangning187');// 这里面的字符串只是一个标志,Symbol返回的值都是唯一的
let s3 = Symbol('zhangning187');
console.log(s2 == s3);// false,确定唯一性


//Symbol.for()方法创建,这是一个对象,这种方式可以得出唯一的Symbol值
let s6 = Symbol.for('zhangning187');
let s8 = Symbol.for('zhangning187');
console.log(s6 ==s8);// true  得到唯一的Symbol值

对象添加Symbol类型的属性

let zn = {


      up: function(){},
      down: function(){},
      name: 'zhangning187',
      age: 24
    }
    // 向对象zn中添加 up down 方法
    // zn.up = function(){}// 这个可以添加但是不确定zn中是否存在up方法,可能会覆盖原来的up方法
    // 这时候需要考虑通过Symbol添加唯一的方法
    // 声明一个对象
    let methods = {


      up: Symbol(),
      down: Symbol()
    }
    zn[methods.up] = function(){


      console.log('我可以爬楼');
    }
    zn[methods.down] = function(){


      console.log('我可以下楼');
    }
    console.log(zn);// 已经添加唯一的方法 up down

    
    let UZI = {


      name: '自豪',
      // Symbol(): function(){},// 这里不能这样直接使用, Symbol()是一个表达式,是一个动态的
      [Symbol('lol')]: function(){


        console.log('我会打lol');
      },
      [Symbol('篮球')]: function(){// Symbol()中还可以添加描述字符串


        console.log('我可以打篮球')
      }
    }
    console.log(UZI);

11.迭代器(Iterator)

迭代器是一种接口,为各种不同的数据结构提供统一的访问机制,任何数据结构只要部署Iterator接口,就可以完成遍历操作

1.ES6创造了一种新的遍历命令for…of循环,Iterator接口提供for…of消费

2.原生具备iterator接口的数据

Array Arguments Set Map String TypedArray NodeList

3.工作原理

  • a 创建一个指针对象,指向当前数据结构的起始位置
  • b 第一次调用对象的next方法,指针自动指向数据结构的第一个成员
  • c 接下来不断调用next方法,指针一直往后移动,直到指向最后一个成员
  • d 每调用next方法返回一个包含value和done属性的对象

自定义遍历数据的时候,要想到迭代器

12.生成器函数的声明与调用

生成器函数是ES6提供的一种异步编程解决方案,与传统函数完全不同,就是一个特殊的函数

// 声明    
function * gen(){// * 可以靠左,也可以靠右,还可以放在中间
  // console.log('hello');
  yield '2020lpl牛批';// yield 语句可以算作函数代码的分隔符
  let two = yield ‘uzi 退役了’;
  console.log(two);
  yield '湖人总冠军';
}
// 执行
let iterator = gen();
// console.log(iterator);// 返回结果是一个迭代器对象
console.log(iterator.next());// 需要执行迭代器对象中的next()方法,才会执行生成器函数
console.log(iterator.next());// 每个next()只会执行里面一个yield语句,这个会输出 ‘uzi 退役了’
// 传递参数 参数将作为上一个yield语句的返回结果
console.log(iterator.next('AAA'));// 第三次调用传递的参数将作为第二个yield 的返回结果 打印为AAA
// 使用for of循环遍历输出
for(let v of gen()){
  console.log(v);// 依次输出yield语句中的值
}

案例:1s输出 111 2s输出 222 3s输出 333

    function one(){
      setTimeout(()=>{
        console.log('111')
        iterator.next();
      }, 1000)
    }
    function two(){
      setTimeout(()=>{
        console.log('111')
        iterator.next();
      }, 1000)


    }
    function three(){
      setTimeout(()=>{
        console.log('111')
      }, 1000)


    }
    // 生成器函数
    function * gen(){
      yield one();
      yield two();
      yield three();
    }
    // 调用生成器函数
    let iterator = gen();
    iterator.next();

13.Promise 基本使用

Promise 是一个构造函数,用来封装异步操作并可以获取其成功或失败的结果

  • 1、 Promise 构造函数  Promise(excutor){}
  • 2、 Promise.prototype.then  有两个参数,两个参数都是函数,

Promise对象成功状态执行then中第一个函数,失败执行第二个函数

  • 3、 Promise.prototype.catch
    // 实例化
    const p = new Promise((resolve, reject) => {
      // 通过resolve,reject这两个函数来改变Promise对象的状态,
      // resolve会改变p的状态为成功,reject会改变p的状态为失败,然后去执行then里面的方法
      // 执行异步操作,以定时器为例,定时器也是异步
      setTimeout(()=>{
        //let data = '异步执行成功';
        // resolve(data);// 调用resolve函数, p会变成一个成功的状态,会执行then中的第一个方法
        let err = '执行失败';
        reject(err);// 调用reject函数,p会变成一个失败的状态,会执行then中的第二个方法
      }, 1000)
    })
     // 成功会调用 promise 对象 then 方法
    p.then(value => {// 成功
      // console.log(value);// 控制台打印:异步执行成功
    }, reason => {// 失败
      console.error(reason)
    })

Promise.prototype.then 特性

    // then方法的返回结果是Promise对象,对象状态由回调函数的执行结果决定
    const p = new Promise((resolve, reject) => {
      setTimeout(()=>{
        resolve('成功');
        reject('失败');
      }, 1000);
    });
    // then 的返回结果是一个Promise对象,就是result也是一个Promise对象,它的状态由函数的执行结果决定的
    const result  = p.then(value => {
      console.log(value);
      // 1.如果返回的结果是 非Promise 类型的属性,状态为成功,返回值return 中的值
      // 如果不写return,函数内部不写return返回结果是undefined,也不是Promise对象,状态也是成功
      // return 123;
      // 2.是 promise 对象, 该对象返回的状态就决定了then方法返回promise对象状态
      return new Promise((resolve, reject)=>{
        // resolve('ok');// then方法返回promise对象状态为成功
        reject('no');// then方法返回promise对象状态为失败
      })
      // 3.抛出错误  then方法返回promise对象状态为失败,错误值为抛出错误的值
      throw new Error('出错了');
    }, reason => {
      console.err(reason);
    });
    console.log(result);
    // 综上总结,then方法可以链式调用  可以改变回调域的现象
    p.then(value=>{}, reason=>{})
      .then(value()=>{}).then();

举例:多个请求都返回之后,获取其中的数据

    const p = new Promise((resolve, reject)=>{
      resolve('第一次返回成功')
    });
    p.then(value=>{
      return new Promise((resolve, reject)=>{
        resolve([value, '第二次返回成功'])
      });
    }).then(value=>{
      return new Promise((resolve, reject)=>{
        resolve([...value, '第三次返回成功'])
      });
    }).then(value=>{
      console.log(value);// 返回值为三次请求都返回成功以后的值
    });

14.集合set

新的数据结构Set(集合),它类似于数组,成员的值都是唯一的,集合实现了iterator接口,所以可以使用扩展运算符和for of遍历

集合的属性和方法

  • 1 size  返回集合的元素个数
  • 2 add  添加一个新元素,返回当前集合
  • 3 delete  删除元素,返回boolean值
  • 4 has  检测集合中是否包含某个元素,返回boolean值
  • 5 clear  清空
    // 声明
    let s = new Set();
    let s2 = new Set([1, 2, 3, 6, 7]);
    console.log(s2);// 5
    s2.add(8);// 添加新元素 
    console.log(s2);// 输出 {1, 2, 3, 6, 7, 8}
    s2.delete(8);
    console.log(s2);// 输出 {1, 2, 3, 6, 7}
    console.log(s2.has(8));// false
    // s2.clear();// 清空


    let arr = [1, 2, 3, 3, 3, 6, 6, 8];
    let arr2 = [1, 3, 6, 7, 8];
    // 数组去重
    let result = [...new Set(arr)];
    // 交集
    let result = [...new Set(arr)].filter(item => new Set(arr2).has(item));
    // 并集
    let result = [...new Set([...arr, ...arr2])];
    // 差集 arr有arr2中没有
    let result = [...new Set(arr)].filter(item => !(new Set(arr2).has(item)));

15.Map集合

类似于对象,也是键值对的集合,但是 键 不限于字符串,各种类型的值(包括对象)都可以当作键,

map也实现了 iterator 接口,所以可以使用扩展运算符和for of进行遍历

  • 1 size  返回 Map 的元素个数
  • 2 set  增加一个新元素,返回当前Map
  • 3 get  返回键名对象的键值
  • 4 has  检测Map中是否包含某个元素,返回boolean值
  • 5 clear  清空集合,返回undefined
    // 声明
    let m = new Map();
    m.set('name', 'zhangning');
    m.set('change', function(){console.log('变得更努力')});// 键 change 值 一个function
    let key = {company: 'supwisdom'}; 
    m.set(key, [1, 2, 3]);//键 对象 值 数组
    m.size;// 获取m个数
    m.delete('name');// 删除键值对
    m.get('change');// 获取键对应的值
    // m.clear();// 清空
    for(let v of m){console.log(v);}

16.class 类

通过class可以定义类,新的class写法只是让对象原型的写法更加清晰,更像面向对象编程的语法而已。

  • 1 class  声明类
  • 2 constructor  定义构造函数初始化
  • 3 extends  继承父类
  • 4 super  调用父级构造方法
  • 5 static  定义静态方法和属性
  • 6 父类方法可以重写

es5通过 构造函数实例化 对象的方法

    // 人
    function People(name, sex) {
      this.name = name;
      this.sex = sex;
    }
    // 这个height这种添加方式是属于函数对象的,不属于实例对象,这样的属性称之为静态成员
    People.height = '180';
    People.prototype.height1 = '100';
    // 添加方法
    People.prototype.play = function(){
      console.log('打篮球');
    }
    let zn = new People('zhangning', '男');
    zn.play();// 输出 打篮球
    console.log(zn);
    console.log(zn.height);// 输出 undefined 
    console.log(zn.height1);// 输出 100,必须通过prototype添加才能添加到实例对象上

通过class实现

    class People{
      // 静态属性 static,对于static 标注的方法属于类,不属于实例对象
      static height = '100';
      static change(){
        console.log('我可以改变世界');
      }
      // 构造方法 名字不能更改(在使用new People的时候会自动执行实例对象上的constructor方法)       
      constructor(name, sex){
        this.name = name;
        this.sex = sex;
      }
      // 添加方法必须使用该语法,不能使用es5的完整形式(play: function(){} 这种形式不支持,必须使用play()形式)
      // 成员属性
      play(){
        console.log('打篮球');
      }
    }
    let zn = new People('zhangning', '男');
    console.log(zn);
    console.log(zn.height);// undefined  static 标注的方法属于类,不属于实例对象
    console.log(People.height);// 100  

使用es5构造函数实现继承

    // 举例 chinese 继承 People 属性
    function People(name, sex) {
      this.name = name;
      this.sex = sex;
    }
    People.prototype.play = function(){
      console.log('打LOL');
    }
    function Student(name, sex, like, height){
      // 通过call方法,改变this值,this指向chinese中的this,也就是chinese的一个实例对象
      People.call(this, name, sex);
      this.like = like;
      this.height = height;
    }
    // 设置子集构造函数原型
    Student.prototype = new People;// 这样就会有父级的一个方法
    Student.prototype.constructor = Student;// 做一个校正,没有这行代码也无所谓
    // 声明子类方法
    Student.prototype.photo = function(){
      console.log('去拍照');
    }
    // 实例化
    const zn = new Student('zhangning', '男', '打篮球', '187');
    console.log(zn)

使用es6 class 类 实现继承 及 父类方法的重写

    // 声明父类
    class People{
      // 父类构造方法
      constructor(name, sex) {
        this.name = name;
        this.sex = sex;
      }
      // 父类成员属性
      play(){
        console.log('打LOL');
      }
    }
    // 声明子类 使用extends 继承父类
    class Student extends People {
      // 构造方法
      constructor(name, sex, like, height){
        super(name, sex);// super 就是父类的constructor构造函数,这样调用
        this.like = like;
        this.height = height;
      }
      photo(){
        console.log('去拍照');
      }
      // 对父类中的play方法进行重写,子类是不能去调用父类的同名方法的,


      play(){
        // super(); 不允许,在普通的成员方法里面是不能出现super()去调用父类的同名方法的,会报错,只能完全重写
        console.log('我会打LOL,还会打篮球');
      }
    }
    const zn = new Student('zhangning', '男', '打篮球', '187');
    console.log(zn)

class 中 getter 和 setter 设置

    class People{
      get like(){
        return '打篮球';
      }
      set like(newVal){
        // 通过传过来的newVal值,进行操作,改变 like
        console.log('改变like值');
      }
    }
    let p = new People();
    console.log(p.like)// 输出 打篮球
    p.like = 'LOL';// 然后通过 set like 进行操作

17.对象数值扩展

Object.is

判断两个值是否完全相等

Object.is(1, 1);// true

和 === 很相似,唯一区别就是 NaN === NaN 为 false, Object.is(NaN, NaN) 为true

Object.assign

对象的合并

const c1 = {name: 'znn'};
const c2 = {name: 'zhangning', height: 187};
Object.assign(c1, c2);// 如果两个对象中存在相同属性,c2 中覆盖c1中的属性内容

Object.setPrototypeOf

设置原型对象

const zn = {
    name: 'zhangning',
}
const p = {
    h: true,
    sfsfdf: 'fasfasdf'
}
Object.setPrototypeOf(zn, p);// 设置 zn 的原型里面有 p
Object.getPrototypeOf(zn);// 获取 zn 的原型
console.log(zn);// 打印看下

18.数组扩展

Array.from()

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

let arrayLike = {  
  '0': 'a',  
  '1': 'b',  
  '2': 'c',  
  length: 3  
};  
// ES5 的写法  
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']  
// ES6 的写法  
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']  

  
// NodeList 对象  
let ps = document.querySelectorAll('p');  
Array.from(ps).forEach(function (p) {  
  console.log(p);  
});  
// arguments 对象  
function foo() {  
var args = Array.from(arguments);  
// ...  
}  


//字符串转换为字符数组str.split('')  
Array.from('hello')  // ['h', 'e', 'l', 'l', 'o']  
let namesSet = new Set(['a', 'b'])  
Array.from(namesSet) // ['a', 'b']  

  
Array.from({ length: 3 });  // [ undefined, undefined, undefined ] 

对于还没有部署该方法的浏览器,可以用Array.prototype.slice方法替代:

const toArray = (() =>
  Array.from ? Array.from : obj => [].slice.call(obj)
)();

Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。

Array.from(arrayLike, x => x * x);  
//  等同于  
Array.from(arrayLike).map(x => x * x);  
Array.from([1, 2, 3], (x) => x * x)  
// [1, 4, 9]  
//Array.from回调函数
var arr1 = Array.from([1,2,3], function(item){
    return item*item;
});
var arr2 = Array.from([1,2,3]).map(function(item){
    return item*item;
});
var arr3 = Array.from([1,2,3], (item) => item*item);


console.log(arr1); //[ 1, 4, 9 ]
console.log(arr2); //[ 1, 4, 9 ]
console.log(arr3); //[ 1, 4, 9 ]

值得提醒的是,扩展运算符(…)也可以将某些数据结构转为数组。

// arguments 对象  
function foo() {  
  var args = [...arguments];  
}  
// NodeList 对象  
[...document.querySelectorAll('div')]  

Array.of()

Array.of方法用于将一组值,转换为数组。Array.of总是返回参数值组成的数组。如果没有参数,就返回一个空数组。

Array.of基本上可以用来替代Array()或new Array(),并且不存在由于参数不同而导致的重载。它的行为非常统一。

这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异

Array() // []  
Array(3) // [, , ,]  
Array(3, 11, 8) // [3, 11, 8]  

  
Array.of() // []  
Array.of(3) // [3]  
Array.of(3, 11, 8) // [3,11,8]  

  
Array.of(3).length // 1   
Array.of(undefined) // [undefined]  
Array.of(1) // [1]  
Array.of(1, 2) // [1, 2]  

Array.of方法可以用下面的代码模拟实现:

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

find() 和 findIndex()

数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。

[1, 4, -5, 10].find((n) => n < 0)  
// -5  
[1, 5, 10, 15].find(function(value, index, arr) {  
    return value > 9;  
}) // 10  

上面代码中,find方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。

[1, 5, 10, 15].findIndex(function(value, index, arr) {  
    return value > 9;  
}) // 2  

这两个方法都可以接受第二个参数,用来绑定回调函数的this对象。

另外,这两个方法都可以发现NaN,弥补了数组的IndexOf方法的不足。

[NaN].indexOf(NaN)  
// -1  
[NaN].findIndex(y => Object.is(NaN, y))  
// 0  

fill()

fill()方法使用给定值,填充一个数组。

['a', 'b', 'c'].fill(7)  
// [7, 7, 7]  
new Array(3).fill(7)  
// [7, 7, 7]  
['a', 'b', 'c'].fill(7, 1, 2)  
// ['a', 7, 'c']  

上面代码表明,fill方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。

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

['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']

entries() , keys() 和 values()

ES6 提供三个新的方法 —— entries(),keys()和values() —— 用于遍历数组。它们都返回一个遍历器对象,可以用for…of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。

for (let index of ['a', 'b'].keys()) {  
    console.log(index);  
}  
// 0  
// 1  
for (let elem of ['a', 'b'].values()) {  
    console.log(elem);  
}  
// 'a'  
// 'b'  
for (let [index, elem] of ['a', 'b'].entries()) {  
    console.log(index, elem);  
}  
// 0 "a"  
// 1 "b"  

如果不使用for…of循环,可以手动调用遍历器对象的next方法,进行遍历。

let letter = ['a', 'b', 'c'];  
let entries = letter.entries();  
console.log(entries.next().value); // [0, 'a']  
console.log(entries.next().value); // [1, 'b']  
console.log(entries.next().value); // [2, 'c']  

includes()

ES5中,我们常用数组的indexOf方法,检查是否包含某个值。indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于 -1 ,表达起来不够直观。二是,它内部使用严格相当运算符( === )进行判断,这会导致对NaN的误判。

[NaN].indexOf(NaN)  
// -1  
includes使用的是不一样的判断算法,就没有这个问题。  
[NaN].includes(NaN)  
// true 

Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。该方法属于 ES7 ,但 Babel 转码器已经支持。

[1, 2, 3].includes(2); // true  
[1, 2, 3].includes(4); // false  
[1, 2, NaN].includes(NaN); // true 

该方法的第二个参数表示搜索的起始位置,默认为 0 。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为 -4 ,但数组长度为 3 ),则会重置为从 0 开始。

[1, 2, 3].includes(3, 3); // false  
[1, 2, 3].includes(3, -1); // true  

下面代码用来检查当前环境是否支持该方法,如果不支持,部署一个简易的替代版本。

const contains = (() =>  
Array.prototype.includes  
    ? (arr, value) => arr.includes(value)  
    : (arr, value) => arr.some(el => el === value)  
)();  


contains(["foo", "bar"], "baz"); // => false  

另外, Map 和 Set 数据结构有一个has方法,需要注意与includes区分。

Map 结构的has方法,是用来查找键名的,比如Map.prototype.has(key)、WeakMap.prototype.has(key)、Reflect.has(target, propertyKey)。

Set 结构的has方法,是用来查找值的,比如Set.prototype.has(value)、WeakSet.prototype.has(value)。

数组的空位

数组的空位指,数组的某一个位置没有任何值。比如,Array构造函数返回的数组都是空位。

注意,空位不是undefined,一个位置的值等于undefined,依然是有值的。空位是没有任何值,in运算符可以说明这一点。

0 in [undefined, undefined, undefined] // true
0 in [, , ,] // false

上面代码说明,第一个数组的 0 号位置是有值的,第二个数组的 0 号位置没有值。

ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位。

  • forEach() , filter() , every() 和some()都会跳过空位。
  • map()会跳过空位,但会保留这个值
  • join()和toString()会将空位视为undefined,而undefined和null会被处理成空字符串。
// forEach方法
[,'a'].forEach((x,i) => console.log(i)); // 1


// filter方法
['a',,'b'].filter(x => true) // ['a','b']


// every方法
[,'a'].every(x => x==='a') // true


// some方法
[,'a'].some(x => x !== 'a') // false


// map方法
[,'a'].map(x => 1) // [,1]


// join方法
[,'a',undefined,null].join('#') // "#a##"


// toString方法
[,'a',undefined,null].toString() // ",a,,"

ES6则是明确将空位转为undefined。

//Array.from方法会将数组的空位,转为undefined,也就是说,这个方法不会忽略空位。  
Array.from(['a',,'b'])  // [ "a", undefined, "b" ]  


//扩展运算符(...)也会将空位转为undefined。  
[...['a',,'b']]  // [ "a", undefined, "b" ]  


//copyWithin()会连空位一起拷贝。  
[,'a','b',,].copyWithin(2,0) // [,"a",,"a"]  


//fill()会将空位视为正常的数组位置。  
new Array(3).fill('a') // ["a","a","a"]  


//for...of循环也会遍历空位。  
let arr = [, ,];  
for (let i of arr) {  
    console.log(1);  
}  
// 1  
// 1  
//上面代码中,数组arr有两个空位,for...of并没有忽略它们。如果改成map方法遍历,空位是会跳过的。  


//entries()、keys()、values()、find()和findIndex()会将空位处理成undefined。  
// entries()  
[...[,'a'].entries()] // [[0,undefined], [1,"a"]]  
// keys()  
[...[,'a'].keys()] // [0,1]  
// values()  
[...[,'a'].values()] // [undefined,"a"]  
// find()  
[,'a'].find(x => true) // undefined  
// findIndex()  
[,'a'].findIndex(x => true) // 0  
//由于空位的处理规则非常不统一,所以建议避免出现空位。

文章来源于公众号: 猴哥说前端,作者:monkeysoft

以上就是W3Cschool编程狮关于****的相关介绍了,希望对大家有所帮助。

链表反转实现方法,后一种击败了100%的用户!

thbcm阅读(194)

链表反转是一道很基础但又非常热门的算法面试题,它也在《剑指Offer》的第 24 道题出现过,至于它有多热(门)看下面的榜单就知道了。

从牛客网的数据来看,链表反转的面试题分别霸占了【上周考过】和【研发最爱考】的双重榜单,像网易、字节等知名互联网公司都考过,但通过率却低的只有 30%,所以本文我们就来学习一下反转链表的两种实现方法。

排行榜数据:https://www.nowcoder.com/activity/oj

题目

标题:剑指 Offer 24. 反转链表

描述:定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

示例:

输入: 1->2->3->4->5->NULL

输出: 5->4->3->2->1->NULL

LeetCode 链接:https://leetcode-cn.com/problems/fan-zhuan-lian-biao-lcof/

实现方式一:Stack

全部入栈:

因为栈是先进后出的数据结构,因此它的执行过程如下图所示:

最终的执行结果如下图所示:

实现代码如下所示:

public ListNode reverseList(ListNode head) {
    if (head == null) return null;
    Stack stack = new Stack();
    stack.push(head); // 存入第一个节点
    while (head.next != null) {
        stack.push(head.next); // 存入其他节点
        head = head.next; // 指针移动的下一位
    }
    // 反转链表
    ListNode listNode = stack.pop(); // 反转第一个元素
    ListNode lastNode = listNode; // 临时节点,在下面的 while 中记录上一个节点
    while (!stack.isEmpty()) {
        ListNode item = stack.pop(); // 当前节点
        lastNode.next = item;
        lastNode = item;
    }
    lastNode.next = null; // 最后一个节点赋为null(不然会造成死循环)
    return listNode;
}

LeetCode 验证结果如下图所示:

实现方式二:递归

实现代码如下所示:

public static ListNode reverseList(ListNode head) {
    if (head == null || head.next == null) return head;
    // 从下一个节点开始递归
    ListNode reverse = reverseList(head.next);
    head.next.next = head; // 设置下一个节点的 next 为当前节点
    head.next = null; // 把当前节点的 next 赋值为 null,避免循环引用
    return reverse;
}

LeetCode 验证结果如下图所示:

总结

本文我们分别使用了 Stack 和递归的方法实现了链表反转的功能,其中 Stack 的实现方式是利用了栈后进先出的特性可以直接对链表进行反转,实现思路和实现代码都比较简单,但在性能和内存消耗方面都不是很理想,可以作为笔试的保底实现方案;而递归的方式在性能和内存消耗方面都有良好的表现,同时它的实现代码也很简洁,读者只需理解代码实现的思路即可。

文章来源于公众号:Java中文社群,作者:磊哥

以上就是W3Cschool编程狮关于链表反转实现方法,后一种击败了100%的用户!的相关介绍了,希望对大家有所帮助。

2020年面试会遇到的手写 Pollyfill 都在这里了

thbcm阅读(208)

最近会把前阵子自己复盘归类整理的这次跳槽面试遇到的所有题目发布到公众号,这是第一篇。不要惊讶,上次跳槽考的也基本是这些题目,时间长了会忘,你只是需要一个清单!

new

测试用例:

function Fn (name) {
  this.name = name
}
console.log(myNew(Fn('lulu')))

实现:

function myNew () {
  const obj = {}
  const Fn = Array.prototype.shift.call(arguments)
  // eslint-disable-next-line no-proto
  obj.__proto__ = Fn.prototype
  const returnVal = Fn.apply(obj, arguments)
  return typeof returnVal === 'object' ? returnVal : obj
}

bind

测试用例:

this.x = 9
const obj = {
  x: 81,
  getX: function () {
    return this.x
  }
}
console.log(obj.getX()) // 81


const retrieveX = obj.getX
console.log(retrieveX()) // 9


const boundGetX = retrieveX.bind(obj)
console.log(boundGetX()) // 81

实现:

Function.prototype.mybind = function () {
  const outerArgs = Array.from(arguments)
  const ctx = outerArgs.shift()
  const self = this
  return function () {
    const innerArgs = Array.from(arguments)
    return self.apply(ctx, [...outerArgs, ...innerArgs])
  }
}

instanceof

测试用例:

console.log(myInstanceof("111", String)); //false
console.log(myInstanceof(new String("111"), String));//true

实现:

function myInstanceof(left, right) {
    //基本数据类型直接返回false
    if(typeof left !== 'object' || left === null) return false;
    //getProtypeOf是Object对象自带的一个方法,能够拿到参数的原型对象
    let proto = Object.getPrototypeOf(left);
    while(true) {
        //查找到尽头,还没找到
        if(proto == null) return false;
        //找到相同的原型对象
        if(proto == right.prototype) return true;
        proto = Object.getPrototypeOf(proto);
    }
}

debounce

在规定时间内函数只会触发一次,如果再次触发,会重新计算时间。

/*** 
 * @description 防抖函数
 * @param func 函数
 * @param wait 延迟执行毫秒数
 * @param immediate 是否立即执行
 * */
function debouncing(func, wait = 1000, immediate = true) {
    let timer = null;
    return function () {
        let args = arguments;
        let context = this;
        if (timer) {
            clearTimeout(timer);
        }
        if (!immediate) {
            //第一种:n秒之后执行,n秒内再次触发会重新计算时间
            timer = setTimeout(() => {
                //确保this指向不会改变
                func.apply(context, [...args]);
            }, wait);
        } else {
            //第二种:立即执行,n秒内不可再次触发
            let callnew = !timer;
            timer = setTimeout(() => {
                timer = null;
                console.log('kaka')
            }, wait);
            if (callnew) func.apply(context, [...args])
        }
    }
}


function fn() {
    console.log('debluncing')
}


let f1 = debouncing(fn, 1000);


setInterval(() => {
    f1()
}, 1000);

throttle

节流指的是函数一定时间内不会再次执行,用作稀释函数的执行频率。

/**
 * @description 节流函数
 * @param func 函数
 * @param wait 延迟执行毫秒数
 * @param type 1:时间戳版本 2: 定时器版本
 *  */
function throttle(func, wait = 1000, type = 1) {
    if (type === 1) {
        let timeout = null;
        return function () {
            const context = this;
            const args = arguments;
            if (!timeout) {
                timeout = setTimeout(() => {
                    timeout = null;
                    func.apply(context, [...args]);
                }, wait);
            }
        }
    } else {
        let previous = 0;
        return function () {
            const context = this;
            const args = arguments;
            let newDate = new Date().getTime();
            if (newDate - previous > wait) {
                func.apply(context, [...args]);
                previous = newDate;
            }
        }
    }


}


function fn() {
    console.log('throttle')
}


const f1 = throttle(fn);


setInterval(() => {
    f1()
}, 100);

deepClone

测试用例:


const map = new Map()
map.set('key', 'value')
map.set('name', 'kaka')


const set = new Set()
set.add('11').add('12')


const target = {
  field1: 1,
  field2: undefined,
  field3: {
    child: 'child'
  },
  field4: [
    2, 8
  ],
  empty: null,
  map,
  set
}
target.target = target
const target1 = deepClone(target)
target1.a = 'a'
console.log('

发现VSCode的一个bug,微软工程师竟然凌晨回复…

thbcm阅读(216)

最近遇到一个有意思的bug,是关于VSCode编辑器插件的,赶项目时间非常紧,说实话在这时平常用的顺手的IDE出问题非常影响心情。「这就像是你开在高速路上,吃着火锅唱着歌,突然轮胎爆了,你说气不气人」。

不过在找bug和推动修复bug的过程有点意思,「通过一系列尝试最终定位和复现了bug,并且给这个项目的微软官方仓库提了issue,最终在最新版本得到了修复,把这个有趣的过程分享给大家」

「也给大家演示一下如何通过提 issue 的方式参与到开源项目中」,当然,参与开源项目的方式有多种,「你可以给项目贡献源码,甚至作为大使帮助推广项目,找到项目的bug进而提issue也是一种参与方式」,总之先参与进来,才能发现开源的乐趣!

诡异的报错

上周,又是一个在公司的夜晚,好像和平常没啥区别,柠檬哥在加班ing,飞快的敲打着自己的机械键盘,熟练的用 VSCode 浏览着项目源码,顺手按下F12+Shift 想看看这个函数在哪些地方被引用了,诡异的事情发生了,这VSCode它竟然不听使唤了,查不出引用的结果了,并且终端提示如下错误:

快速信息操作失败: FE: 'Compiler exited with error - No IL available'
快速信息操作失败: FE: 'Compiler exited with error - No IL available'

一开始我以为是单个工程解析问题,「不慌,问题不大」

后来换了个工程尝试,「不论我如何的反复摩擦我洁白的键盘帽,始终不能出来查找引用的结果界面」,这时才发现,粗大事了。工欲善其事必先利其器,虽然进度有点赶,还是停下来康康是谁在捣鬼?

如果不能查找引用的话,那会对编码和阅读源码带来很大的不便,「这个功能算的上是IDE的基础功能了,如果连这功能都废了,那我要你这VSCode有何用」?如果不能修复的话我估计要跑抛弃它,用回 Visual Studio 或 CLion。

但是VSCode远程开发是真的香,并且已经习惯了VSCode操作,在放弃之前还想挣扎下,看还能不能抢救?不过如果实在不行,也没时间死磕,项目还要继续,大不了换个 IDE 继续玩,甚至都想好了以面再也不说VSCode香了。

一起来找bug呀

虽然这个插件不是我写的,但我按照一般程序员排查bug的思路,通过下面几个步骤一步步来找到问题原因,最终并推动官方的版本更新来修复,一起来看看吧。

软件问题?

「首先,来看看是不是VSCode版本升级导致的问题」。按下面的操作,我检查 VSCode 的版本信息。

仔细核对版本号和官网的区别,对比问题出现的时间前后都没有升级过新版本。

OK,「应该不是 VSCode 版本升级导致的问题」

配置问题?

既然不是 VSCode 升级版本导致的问题,那就奇了怪了,白天还好好的晚上突然咋就不行了呢?难道这插件也不想加班了?我陷入了沉思,不过马上灵机一动,「会不会小心改了C++环境配置文件出了问题」

「这里有个知识点记下,要考」。VSCode中有一个叫c_cpp_properties.json的配置文件,这个文件主要用于配置C/C++工程的基础信息,比如:「预定义宏、指定编译器路径、预定义头文件搜索路径等」

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "/lemon/handsome/thirdparty/**",
                "/lemon/smart/inc/**"
            ],
            "defines": [], 
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "c11",
            "cppStandard": "c++14",
            "intelliSenseMode": "gcc-x64"
        }
    ],
    "version": 4
}

机智如我,肯定是这个工程的include 搜索路径配置的有问题,才导致查找引用失败了,赶紧去检查一眼配置文件,于是熟练的敲下Ctrl+Shift+P 「查找所有命令和配置」「敲黑板!这个命令很常用,背下来」),输入关键字c++ Edit 果然匹配到了配置文件,打开就是上面的配置文件。

但是看起来文件路径好像是对的,不管了,死马当活马医,先全部删除重新配置一遍看看效果,一顿操作之后兴奋的检查有没有用,然并卵,还是那个该死的提示FE: 'Compiler exited with error - No IL available',心态有点崩。

环境问题?

发现这个问题确实有点诡异,走到这一步,「我几乎可以断定是Linux开发环境出了问题,但是不确定是我的机器环境问题还是 Linux下 VSCode 本身问题,那怎么办呢?先来证明是Linux下才出的问题吧」

我就尝试不开远程开发模式,把远程Linux机器上的工程直接拉到宿主机本地文件夹,然后用VSCode打开宿主机上的本地工程,「它竟然工作的很好,完全没有出现什么错误提示,到这,已经完全可以确定这个bug只在Linux环境下出现了」

「夜已深,起来喝杯水,看了下时间,加班班车快到末班了。」

事已至此,看来真的要关掉远程开发,在本地重新配置所有工程了,表面上还是劝自己再找找原因,没事,问题不大。

插件问题?

喝完水,我坐下来继续想,「会不会是C++扩展出了问题呢?大家都知道VSCode只能说是一个编辑器,能够让他变身C++ IDE完全是有背后的C++插件或者叫扩展的支持」

就是下面黑黑的这货,它是VSCode能够支持C++开发背后的男人,众所周知VSCode是微软亲儿子,看看这个插件作者Microsoft 看来也是微软自家人开发的插件,发布之前肯定是经过严格测试的,问题不大。

不过现在谁也不能相信,即使是微软自家的插件也不能信任了,假装冷静分析一波。

经过严谨的思考(然而并没有),最终决定拿出程序员必杀技:「重启试试,卸载重装」

点击卸载,卸载完成,点击安装,重装完成,重启加载,一气呵成。

兴奋的搓小手手,准备再次见证奇迹,WTF,问题依旧没能解决,实话告诉大家,做到这个份上,柠檬哥可以说是已经非常的绝望了。

正道的光

真相只有一个

不管了先回去睡一觉,梦里啥都有,没准第二天白天又有了新思路。

果然第二天我又有了新想法,虽然卸载重装插件没用,但我们程序员还有最后一招:「回退版本」

是时候表演真正的技术了,「资深程序员肯定懂的,常在河边站哪有不湿鞋,版本发布出问题,赶紧回退保命是常规操作」

那我们就有理由怀疑:「微软在发布这个插件最新版本的的时候把一个重要特性搞掉了」。但是这东西发都发出去了,也不是服务端后台服务说回退就能回退的,这个插件如果真是bug也只能等下一个版本修复,还是我们自己来操作回退吧。

找到插件,按下面方法来执行回退操作:

柠檬当时回退的时候还没有出最新的修复版本,装的是有问题的1.0.0版本的插件,「看这个版本号应该是个较大改动的大版本,出问题也正常」

关键是「可以看到这个版本的发布时间点刚好是我发现bug的时间」,这回感觉离真相越来越近了,至少在时间上是吻合的有底气了点,那有理由怀疑是这个插件出了问题,回退到上一个稳定版本0.29.0

「这次奇迹真的出现了,「查找引用功能」它回来了」!而且也没有出现FE: 'Compiler exited with error - No IL available'的报错提示,为了进一步确认自己的判断,我又把插件升级到1.0.0版本(稳了),果然又出现了刚才的问题。

「至此,这个bug算是定位成功,并且可复现验证,暂时的解决方法是回退到上一个稳定版本」

离线安装

另外提醒一下,公司其他同学也遇到这个问题,我在帮其他同学解决这个问题的时候,发现有些人直接升级可能会有网络问题,导致在线升级不了,报错:

这时候可以去上面我发的官网下载离线版本插件安装包,下完之后,按照下面的操作升级即可,效果和在线升级一样。

注意了,由于一些众所周知的原因,「如果你打不开官网或者下载速度很慢,可以加文末我个人微信,备注「下载插件」我发给你已经下载好的插件」

推动版本更新

提issue报bug

这就完了吗?当然不是。「费了我这么大功夫找出来的bug,不要再浪费其他人时间了,所以我决定去微软这个插件的官方仓库去给他们提 issue,这里给不了解 Github 开源项目的同学科普下什么是issue ? 「上课了,看黑板」

Github项目的 issue 用于团队协作管理,可以把将要实现的 feature 或者要修复的 bug 通过提 issue的形式记录下来,所以我们如果发现开源项目的bug,可以通过给开源项目提issue的形式报告这个bug,提醒项目团队修复跟进。

这里给出微软官方C/C++ 插件的github仓库地址:https://github.com/microsoft/vscode-cpptools

我去写下了下面这个issue ,虽然是英文描述的,大家应该都能看得懂我就不逐字翻译了,计算机相关的英文来回就那么几个单词,看多了就会写,大意就是描述了我遇到的bug和问题出现时的环境配置信息,方便他们定位和复现问题。

issue 标题:C/C++ Extension 1.0.0 some feature Not working When using in Remote-SSH remote development #6176

并且详细描述了我遇到的问题,其实经过上面一顿操作,柠檬肯定是他们这个版本有问题,「但还要友好沟通推进问题尽快解决才是目的,写代码的何苦为难写代码的」,没有直接说他们有问题,而是委婉的问了下 I wonder if there is a problem with this latest Extension ? 哈哈

完美解决

我提issue的时候是中午吃饭的时候12点左右,那时美国那边应该还是凌晨,我想肯定没这么快有回复了,国外程序员小哥都还在睡觉呢,怎么也得早上上班才能看到之后回复,「但是万万没想到在下午5点左右就收到了回复,果然神速」

「不过,等等,好像哪里有点不对劲」,注意上面图中具体时间已经没显示了,只是显示一个 2 days ago,在我看到消息通知的时候有点诧异处理这么神速,好奇去翻开处理issue老哥的 github 主页介绍。

回复的这位是微软VS Code C++ Extension的软件开发工程师,然后定位是美国的Redmod, WA ,特意去查了当时的美国时间是05:03,这位老哥是在凌晨5点钟处理的这个bug。。。「这也太优秀了吧,果然大佬们都是半夜写代码不用睡觉的,看到凌晨五点的太阳我信了」

复盘一下

「到了这里,这个bug从出现在我的机子上,到定位查找,最终修复算是完美的解决」。这里附上官方的版本链接:https://github.com/microsoft/vscode-cpptools/releases ,里面有提到本次修复的bug。

按照最新的1.0.1 版本发布说明,「如果你使用 Linux/MAC 版本的VSCode 或者像我这样用远程开发的方式从宿主机使用Linux版本,可能会遇到我文中说的bug」,我是在0.29.0自动升级到1.0.0发现的bug,于是给1.0.0版本报了个issue,微软官方在1.0.1版本修复了上述的bug,「一张图看清时间线」

柠檬在这里建议:正在使用0.29.0版本插件的同学不升也没啥大问题,但如果你用的是1.0.0版本,那就要注意了,这个版本在本文描述的场景下是有问题的,还是及时升级到最新版本为好

其实今天这样的场景也是程序员日常工作的真实写照,我们每天都在处理类似的事情,奇奇怪怪的bug有一千种产生方式,要做的就是把他怎么产生的原因找出来,从这个角度来说程序员个个都是福尔摩斯

文章来源于公众号:后端技术学堂 ,作者:LemonCoder

以上就是W3Cschool编程狮关于发现VSCode的一个bug,微软工程师竟然凌晨回复…的相关介绍了,希望对大家有所帮助。

全面认识ECMAScript模块

thbcm阅读(209)

关于ES模块,本文将提供一些详细的解读,希望对你有所帮助!

什么是ES模块?

ECMAScript模块(简称ES模块)是2015年推出的 JavaScript 中代码重用的机制。在高度碎片化的 JavaScript 模块场景中,它终于成为了标准。

在2015年之前, JavaScript 还没有一个标准的代码重用机制。这方面曾有过很多标准化的尝试,导致这些年乱七八糟的碎片化。

你可能听说过 AMD 模块、UMD 或者 CommonJS。没有明显的赢家。终于,随着 ECMAScript 2015,ES模块登陆语言。

我们现在有了一个 “官方 “的模块系统。

ECMAScript模块无处不在?

理论上,ECMAScript 模块应该普遍适用于所有 JavaScript 环境。实际上,浏览器仍然是ES模块的主要目标。

2020年5月,Node.js v12.17.0 发货时,支持 ECMAScript 模块,没有标志。这意味着我们现在可以在 Node.js 中使用导入和导出,而无需任何额外的命令行标志。

在 ECMAScript 模块在任何 JavaScript 环境中普遍工作之前,还有很长的路要走,但方向是正确的。

ES模块是怎样的?

一个ES模块就是一个简单的文件,我们可以声明一个或多个出口。以这个虚构的 utils.js 为例。

// utils.js
export function funcA() {
  return "Hello named export!";
}


export default function funcB() {
  return "Hello default export!";
}

我们这里有两个导出。

第一个是一个命名的导出,后面是一个默认的导出,表示为导出默认。

假设我们的项目文件夹中住着这个名为 utils.js 的文件,我们可以在另一个文件中导入这个模块提供的对象。

如何从ES模块导入

假设我们在项目文件夹中还有一个名为 consumer.js 的文件。要导入 utils.js 所暴露的函数,我们可以这样做。

// consumer.js
import { funcA } from "./util.js";

这种语法是一种命名的导入方式,与命名的导出方式有异曲同工之妙。

如果要导入定义为默认导出的 funcB,我们可以这样做:

// consumer.js
import funcB from "./util.js";

如果我们想在一个文件中同时导入默认导出和命名导出,我们可以将其压缩为:

// consumer.js
import funcB, { funcA } from "./util.js";


funcB();
funcA();

我们也可以用 star 导入整个模块。

import * as myModule from "./util.js";


myModule.funcA();
myModule.default();

要注意,在这种情况下,必须显式调用默认导出。

要从远程模块导入。

import { createStore } from "https://unpkg.com/redux@4.0.5/es/redux.mjs";


const store = createStore(/* do stuff */)

浏览器中的ECMAScript模块

现代浏览器支持 ES 模块,尽管有一些注意事项。要加载一个模块,请在脚本标签的 type 属性中添加模块。








    
    ECMAScript modules in the browser




<p id="el">The result is: </p>




    import { appendResult } from "./myModule.js";


    const el = document.getElementById("el");
    appendResult(el);



这里 myModule.js 是同一个项目文件夹下的一个简单模块。

export function appendResult(element) {
  const result = Math.random();
  element.innerText += result;
}

虽然可以直接在浏览器中使用ES模块,但现在捆绑 JavaScript 应用的任务仍然是 webpack 等工具的专属,以获得最大的灵活性、代码拆分和对旧浏览器的兼容性。

动态导入

ES 模块是静态的,这意味着我们无法在运行时更改导入。有了2020年登陆的动态导入,我们可以根据用户的交互动态加载我们的代码(webpack在ECMAScript 2020中提供动态导入功能之前就已经提供了)。

考虑一个简单的 HTML,它可以加载一个脚本。








    
    Dynamic imports




<button id="btn">Load!</button>





也可以考虑用几个导出的 JavaScript 模块。

// util.js
export function funcA() {
  console.log("Hello named export!");
}


export default function funcB() {
  console.log("Hello default export!");
}

如果要动态加载这个模块,也许点击一下,我们可以这样做。

// loader.js
const btn = document.getElementById("btn");


btn.addEventListener("click", () => {
  // loads named export
  import("./util.js").then(({ funcA }) => {
    funcA();
  });
});

在这里,我们通过重构模块的对象,只加载命名的导出。

({ funcA }) => {}

ES 模块实际上就是 JavaScript 对象:我们可以重构它们的属性,也可以调用它们的任何暴露的方法。

要动态地导入一个默认的导出,我们可以这样做。

// loader.js
const btn = document.getElementById("btn");


btn.addEventListener("click", () => {
  // loads entire module
  // runs default export
  import("./util.js").then((module) => {
    module.default();
  });
});

当整体导入一个模块时,我们可以使用它的所有输出。

// loader.js
const btn = document.getElementById("btn");


btn.addEventListener("click", () => {
  // loads entire module
  // uses everything
  import("./util.js").then((module) => {
    module.funcA();
    module.default();
  });
});

还有一种常见的动态导入方式,我们在文件的顶部提取逻辑。

const loadUtil = () => import("./util.js");


const btn = document.getElementById("btn");


btn.addEventListener("click", () => {
  //
});

在这里,loadUtil 将返回一个 Promise,准备进行链锁。

const loadUtil = () => import("./util.js");


const btn = document.getElementById("btn");


btn.addEventListener("click", () => {
  loadUtil().then(module => {
    module.funcA();
    module.default();
  });
});

动态导入看起来很好,但是它们有什么用呢?

通过动态导入,我们可以拆分我们的代码,只在合适的时刻加载重要的内容。在动态导入登陆JavaScript之前,这种模式是webpack这个模块捆绑器的专属。

像React和Vue这样的前端库,就大量使用了通过动态导入进行代码拆分的方式,在响应事件时加载分块代码,比如用户交互或者路由变化。

JSON文件的动态导入

假设你在代码库的某个地方有一个JSON文件person.json。

{
  "name": "Jules",
  "age": 43
}

现在,你想动态地导入这个文件,以响应一些用户的交互。

由于JSON文件导出的只是一个默认的导出,它不是一个函数,所以你只能像这样访问默认的导出。

const loadPerson = () => import("./person.json");


const btn = document.getElementById("btn");


btn.addEventListener("click", () => {
  loadPerson().then(module => {
    const { name, age } = module.default;
    console.log(name, age);
  });
});

这里,我们从默认的导出中重构name和age。

    const { name, age } = module.default;

使用async/await动态导入

import()语句返回的总是一个Promise,这意味着我们可以对它使用async/await。

const loadUtil = () => import("./util.js");


const btn = document.getElementById("btn");


btn.addEventListener("click", async () => {
  const utilsModule = await loadUtil();
  utilsModule.funcA();
  utilsModule.default();
});

动态导入名称

当用import()导入一个模块时,你可以随心所欲地给它命名,只要保持一致即可。

  import("./util.js").then((module) => {
    module.funcA();
    module.default();
  });

或者:

  import("./util.js").then((utilModule) => {
    utilModule.funcA();
    utilModule.default();
  });

文章来源于公众号:前端开发博客

以上就是W3Cschool编程狮关于全面认识ECMAScript模块的相关介绍了,希望对大家有所帮助。

拜托了,不要再使用!=null判空了!

thbcm阅读(204)

对于 Java 程序员来说,null是令人头痛的东西。时常会受到空指针异常(NPE)的骚扰。连 Java 的发明者都承认这是他的一项巨大失误。

那么,有什么办法可以避免在代码中写大量的判空语句呢?

有人说可以使用 JDK8提供的 Optional 来避免判空,但是用起来还是有些麻烦。

作者在日常工作中,封装了一个工具,可以链式调用对象成员而无需判空,相比原有的if null逻辑 和 JDK8提供的 Optional 更加优雅易用,在工程实践中大大提高了编码效率,也让代码更加的精准和优雅。

不优雅的判空调用

我想从事 Java 开发的小伙伴肯定有遇到过下面这种让人难受的判空逻辑:现在有一个User类,School 是它的成员变量

/**


* @author Axin


* @since 2020-09-20


* @summary 一个User类定义


 * (Ps:Data 是lombok组件提供的注解,简化了get set等等的约定代码)


*/


@Data


public class User {


    private String name;


    private String gender;


    private School school;


    @Data


    public static class School {


        private String scName;


        private String adress;


    }


}

现在想要获得School的成员变量 adress , 一般的处理方式:

public static void main(String[] args) {


    User axin = new User();


    User.School school = new User.School();


    axin.setName("hello");


    if (Objects.nonNull(axin) && Objects.nonNull(axin.getSchool())) {


        User.School userSc = axin.getSchool();


        System.out.println(userSc.getAdress());


    }


}

获取 adress 时要对 School 进行判空,虽然有些麻烦,到也能用,通过 JDK8 提供的 Optional 工具也是可以,但还是有些麻烦。

而下文的 OptionalBean 提供一种可以链式不断地调用成员变量而无需判空的方法,直接链式调用到你想要获取的目标变量,而无需担心空指针的问题。

链式调用成员变量

如果用了本文设计的工具 OptionalBean ,那么上述的调用可以简化成这样:

public static void main(String[] args) {


    User axin = new User();


    User.School school = new User.School();


    axin.setName("hello");


    // 1. 基本调用


    String value1 = OptionalBean.ofNullable(axin)


            .getBean(User::getSchool)


            .getBean(User.School::getAdress).get();


    System.out.println(value1);


}

执行结果:

其中User的school变量为空,可以看到代码并没有空指针,而是返回了null。这个工具怎么实现的呢?

OptionalBean 工具

/**


* @author Axin


* @since 2020-09-10


* @summary 链式调用 bean 中 value 的方法


*/


public final class OptionalBean<T> {


    private static final OptionalBean<?> EMPTY = new OptionalBean<>();


    private final T value;


    private OptionalBean() {


        this.value = null;


    }


    /**


     * 空值会抛出空指针


     * @param value


     */


    private OptionalBean(T value) {


        this.value = Objects.requireNonNull(value);


    }


    /**


     * 包装一个不能为空的 bean


     * @param value


     * @param <T>


     * @return


     */


    public static <T> OptionalBean<T> of(T value) {


        return new OptionalBean<>(value);


    }


    /**


     * 包装一个可能为空的 bean


     * @param value


     * @param <T>


     * @return


     */


    public static <T> OptionalBean<T> ofNullable(T value) {


        return value == null ? empty() : of(value);


    }


    /**


     * 取出具体的值


     * @param fn


     * @param <R>


     * @return


     */


    public T get() {


        return Objects.isNull(value) ? null : value;


    }


    /**


     * 取出一个可能为空的对象


     * @param fn


     * @param <R>


     * @return


     */


    public <R> OptionalBean<R> getBean(Function<? super T, ? extends R> fn) {


        return Objects.isNull(value) ? OptionalBean.empty() : OptionalBean.ofNullable(fn.apply(value));


    }


    /**


     * 如果目标值为空 获取一个默认值


     * @param other


     * @return


     */


    public T orElse(T other) {


        return value != null ? value : other;


    }


    /**


     * 如果目标值为空 通过lambda表达式获取一个值


     * @param other


     * @return


     */


    public T orElseGet(Supplier<? extends T> other) {


        return value != null ? value : other.get();


    }


    /**


     * 如果目标值为空 抛出一个异常


     * @param exceptionSupplier


     * @param <X>


     * @return


     * @throws X


     */


    public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {


        if (value != null) {


            return value;


        } else {


            throw exceptionSupplier.get();


        }


    }


    public boolean isPresent() {


        return value != null;


    }


    public void ifPresent(Consumer<? super T> consumer) {


        if (value != null)


            consumer.accept(value);


    }


    @Override


    public int hashCode() {


        return Objects.hashCode(value);


    }


    /**


     * 空值常量


     * @param <T>


     * @return


     */


    public static<T> OptionalBean<T> empty() {


        @SuppressWarnings("unchecked")


        OptionalBean<T> none = (OptionalBean<T>) EMPTY;


        return none;


    }


}

工具设计主要参考了 Optional 的实现,再加上对链式调用的扩展就是上述的OptionalBean。

getBean 其实是当变量为空时返回了一个 包装空值的 OptionalBean 对象,同时泛型的使用让工具更加易用。

使用手册

可以看到代码中也提供了和 Optional 一样的扩展方法,如 ifPresent()、orElse()等等:

public static void main(String[] args) {


    User axin = new User();


    User.School school = new User.School();


    axin.setName("hello");


    // 1. 基本调用


    String value1 = OptionalBean.ofNullable(axin)


            .getBean(User::getSchool)


            .getBean(User.School::getAdress).get();


    System.out.println(value1);


    // 2. 扩展的 isPresent方法 用法与 Optional 一样


    boolean present = OptionalBean.ofNullable(axin)


            .getBean(User::getSchool)


            .getBean(User.School::getAdress).isPresent();


    System.out.println(present);


    // 3. 扩展的 ifPresent 方法


    OptionalBean.ofNullable(axin)


            .getBean(User::getSchool)


            .getBean(User.School::getAdress)


            .ifPresent(adress -> System.out.println(String.format("地址存在:%s", adress)));


    // 4. 扩展的 orElse


    String value2 = OptionalBean.ofNullable(axin)


            .getBean(User::getSchool)


            .getBean(User.School::getAdress).orElse("家里蹲");


    System.out.println(value2);


    // 5. 扩展的 orElseThrow


    try {


        String value3 = OptionalBean.ofNullable(axin)


                .getBean(User::getSchool)


                .getBean(User.School::getAdress).orElseThrow(() -> new RuntimeException("空指针了"));


    } catch (Exception e) {


        System.out.println(e.getMessage());


    }


}

run一下:

总结

设计了一种可以链式调用对象成员而无需判空的工具让代码更加的精准和优雅,如果本文设计的工具满足了刚好解决你的困扰,那就在项目中使用吧!

文章来源于公众号:Hollis,作者:上帝爱吃苹果

以上就是W3Cschool编程狮关于拜托了,不要再使用!=null判空了!的相关介绍了,希望对大家有所帮助。

阔别两年,webpack 5 正式发布了!

thbcm阅读(186)

HI,继 Vue 3 和 React 17 之后,我又来啦!印记中文已经完成了 webpack v5 中文文档的同步及翻译工作,大家可以无缝进行阅读哦。

文档地址请认准:https://webpack.docschina.org

文档地址请认准:https://webpack.docschina.org

文档地址请认准:https://webpack.docschina.org

重要的事说三遍,我们的文档隶属于官方,我们没有其他的域名哦,并且是与官方进行实时同步。

2020 年 10 月 10 日,webpack 升级至 5.0 版本,并且为官网添加了博客目录。我们及时的进行了同步,此文是我们阅读后总结归纳的版本。话不多说开始正文。

自从 2018 年 2 月,webpack4 发布以来,webpack 就暂时没有更进一步的重大更新,为了保持 API 的一致性,旧的架构没有做太多改变,遗留了很多的包袱。阔别 2 年多后,2020 年 10 月 10 日,webpack 5 正式发布,并带来了诸多重大的变更,将会使前端工程师的构建效率与质量大为提升。

本次重大发布的整体发展方向如下:

  • 尝试用持久性缓存来提高构建性能。
  • 尝试用更好的算法和默认值来改进长期缓存。
  • 尝试用更好的 Tree Shaking 和代码生成来改善包大小。
  • 尝试改善与网络平台的兼容性。
  • 尝试在不引入任何破坏性变化的情况下,
    • 清理那些在实现 v4 功能时处于奇怪状态的内部结构。
  • 试图通过现在引入突破性的变化来为未来的功能做准备,
    • 尽可能长时间地保持在 v5 版本上。

webpack 5Release Note 非常长,本文尝试摘出最简练的信息。

功能清除

清理已弃用的功能

所有在 webpack 4 标记即将过期的功能,都已在该版移除。因此在迁移到 webpack 5 之前,请确保你在 webpack 4 运行的构建不会有任何的功能过期警告。

不再为 Node.js 模块 自动引用 Polyfills

不再为 Node.js 内置模块自动添加 Polyfills。任何项目中有引用 Node.js 内置模块,在 webpack 4 或之前的版本中会自动添加 Polyfills。但 webpack 5 将不会再这样做,webpack会投入更多的精力到前端模块的兼容性工作中。

如果你的代码中有引用这些 Node.js 的模块,要升级到 webpack 5, 将尽量使用前端的模块,或者自行手动添加适合的 Polyfills

而针对那些类库的开发者,请在 package.json 中定义 browser 字段,使类库在前端能适用。

针对长期缓存的优化

确定的 Chunk、模块 ID 和导出名称

新增了长期缓存的算法。这些算法在生产模式下是默认启用的。

chunkIds: "deterministic"``moduleIds: "deterministic"``mangleExports: "deterministic"

该算法以确定性的方式为模块和分块分配短的(3 或 5 位)数字 ID,这是包大小和长期缓存之间的一种权衡。由于这些配置将使用确定的 ID 和名称,这意味着生成的缓存失效不再更频繁。

真正的内容哈希

当使用[contenthash]时,Webpack 5 将使用真正的文件内容哈希值。之前它 “只 “使用内部结构的哈希值。当只有注释被修改或变量被重命名时,这对长期缓存会有积极影响。这些变化在压缩后是不可见的。

更好的开发支持

命名代码块 ID

在开发模式下,默认启用的新命名代码块 ID 算法为模块(和文件名)提供了人类可读的名称。模块 ID 由其路径决定,相对于 context。代码块 ID 由代码块的内容决定。

所以你不再需要使用import(/* webpackChunkName: "name" */ "module")来调试。但如果你想控制生产环境的文件名,还是有意义的。

可以在生产环境中使用 chunkIds: "named" 在生产环境中使用,但要确保不要不小心暴露模块名的敏感信息。

迁移:如果你不喜欢在开发中改变文件名,你可以通过 chunkIds: "natural" 来使用旧的数字模式。

模块联邦

Webpack 5 增加了一个新的功能 “模块联邦”,它允许多个 webpack 构建一起工作。从运行时的角度来看,多个构建的模块将表现得像一个巨大的连接模块图。从开发者的角度来看,模块可以从指定的远程构建中导入,并以最小的限制来使用。

支持崭新的 Web 平台功能

对于 Web 平台的的一些支持 ,Webpack 5 也做了更好的完善更新。

JSON 模块

比如对 JSON 模块,会与现在的提案保持一致,并且要求进行默认的导出,否则会有警告信息。即使使用默认导出,未使用的属性也会被 optimization.usedExports 优化丢弃,属性会被 optimization.mangleExports 优化打乱。

如果想用自定义的 JSON 解析器,可以在 Rule.parser.parse 中指定一个自定义的 JSON 解析器来导入类似 JSON 的文件(例如针对 toml、yaml、json5 等)。

资源模块

Webpack 5 现在已经对表示资源的模块提供了内置支持。这些模块可以向输出文件夹发送一个文件,或者向 javascript 包注入一个 DataURI。无论哪种方式,它们都会给出一个 URL 来工作。

它们可以通过多种方式被使用:

  • import url from "./image.png" 和 在module.rule 中设置 type: "asset"当匹配这样的导入时。(老方法)
  • new URL("./image.png", import.meta.url) (新方式)

选择 “新的方式 “语法是为了允许在没有打包工具的情况下运行代码。这种语法也可以在浏览器中的原生 ECMAScript 模块中使用。

原生 Worker 支持

当把资源的 new URLnew Worker/new SharedWorker/navigator.serviceWorker.register 结合起来时,webpack 会自动为 web worker 创建一个新的入口点(entrypoint)。

new Worker(new URL("./worker.js", import.meta.url))

选择这种语法也是为了允许在没有打包工具的情况下运行代码。这种语法在浏览器的原生 ECMAScript 模块中也可以使用。

URIs

Webpack 5 支持在请求中处理协议。

  • 支持data:。支持 Base64 或原始编码。Mimetype 可以在module.rule中被映射到加载器和模块类型。例如:import x from "data:text/javascript,export default 42"
  • 支持file:
  • 支持http(s):,但需要通过new webpack.experiments.s schemesHttp(s)UriPlugin()选择加入。
    • 默认情况下,当目标为 “web “时,这些 URI 会导致对外部资源的请求(它们是外部资源)。

支持请求中的片段。例如:./file.js#fragment

异步模块

Webpack 5 支持所谓的 “异步模块”。这些模块并不是同步解析的,而是基于异步和 Promise 的。

通过 “import “导入它们会被自动处理,不需要额外的语法,而且几乎看不出区别。

通过require()导入它们会返回一个解析到导出的 Promise。

在 webpack 中,有多种方式来拥有异步模块。

  • 异步的外部资源(async externals)
  • 新规范中的 WebAssembly 模块
  • 使用顶层 Await 的 ECMAScript 模块。

外部资源

Webpack 5 增加了更多的外部类型来覆盖更多的应用:

promise: 一个评估为 Promise 的表达式。外部模块是一个异步模块,解析值作为模块导出使用。

import。原生的 import() 用于加载指定的请求,外部模块是一个异步模块,解析值作为模块导出。外部模块是一个异步模块。

module: 尚未实现,但计划通过 import x from "..." 加载模块。

script: 通过 “ 标签加载一个 url,并从一个全局变量(以及它的可选属性)中获取输出。外部模块是一个异步模块。

全新的 Node.js 生态特性

现在支持 package.json 中的 exportsimports 字段。现在起原生支持 Yarn PnP。

更多细节请参见package exports。

开发体验的提升

经过优化的构建目标(target)

Webpack 5 允许传递一个目标列表,并且支持目标的版本。例如 target: "node14"``target: ["web", "es2020"]

这是一个简单的方法,为 webpack 提供它需要确定的所有信息:

  • 代码块加载机制,以及
  • 支持的语法,如箭头函数

统计

改进了统计测试格式的可读性和冗余性。改进了默认值,使其不那么冗长,也适合大型构建。

进度

ProgressPlugin插件也做了一些优化,现在不仅可以统计模块编译的进度,也可以统计 入口依赖。并且,之前展示进度可能会对构建性能有一定的影响,这次的升级也做了一些性能方面的优化。

自动添加唯一命名

在 webpack 4 中,多个 webpack 运行时可能会在同一个 HTML 页面上发生冲突,因为它们使用同一个全局变量进行代码块加载。为了解决这个问题, 需要为 output.jsonpFunction 配置提供一个自定义的名称。

Webpack 5 确实会从 package.json name 中自动推断出一个唯一的构建名称,并将其作为 output.uniqueName 的默认值。

这个值用于使所有潜在的冲突的全局变量成为唯一。

迁移: 由于 package.json 中有唯一的名称,可将 output.jsonpFunction删除。

自动添加公共路径

Webpack 5 会在可能的情况下自动确定 output.publicPath

Typescript 类型

Webpack 5 从源码中生成 typescript 类型,并通过 npm 包暴露它们。

迁移:删除 @types/webpack。当名称不同时更新引用。

构建优化

嵌套的 tree-shaking

webpack 现在能够跟踪对导出的嵌套属性的访问。这可以改善重新导出命名空间 对象时的 Tree Shaking(清除未使用的导出和混淆导出)。

// inner.js
export const a = 1;
export const b = 2;


// module.js
export * as inner from './inner';
// 或 import * as inner from './inner'; export { inner };


// user.js
import * as module from './module';
console.log(module.inner.a);

在这个例子中,可以在生产模式下删除导出的b

内部模块 tree-shaking

webpack 4 没有分析模块的导出和引用之间的依赖关系。webpack 5 有一个新的选项 optimization.innerGraph,在生产模式下是默认启用的,它可以对模块中的标志进行分析,找出导出和引用之间的依赖关系。

在这样的模块中:

import { something } from './something';


function usingSomething() {
  return something;
}


export function test() {
  return usingSomething();
}

内部依赖图算法会找出 something 只有在使用 test 导出时才会使用。这允许将更多的出口标记为未使用,并从代码包中省略更多的代码。

当设置"sideEffects": false时,可以省略更多的模块。在这个例子中,当 test 导出未被使用时,./something 将被省略。

要获得未使用的导出信息,需要使用 optimization.unusedExports。要删除无副作用的模块,需要使用optimization.sideEffects。可以分析以下标记:

  • 函数声明
  • 类声明
  • 默认导出export default 或定义变量以下的:
    • 函数表达式
    • 类表达式
    • 顺序表达式
    • /*#__PURE__*/ 表达式
    • 局部变量
    • 引入的捆绑(bindings)

反馈:如果你发现这个分析中缺少什么,请报告一个问题,我们会考虑增加它。

使用 eval() 将为一个模块放弃这个优化,因为经过 eval 的代码可以引用范围内的任何标记。这种优化也被称为深度范围分析。

CommonJs Tree Shaking

webpack 曾经不进行对 CommonJs 导出和 require() 调用时的导出使用分析。

webpack 5 增加了对一些 CommonJs 构造的支持,允许消除未使用的 CommonJs 导出,并从 require() 调用中跟踪引用的导出名称。

支持以下构造:

  • exports|this|module.exports.xxx = ...
  • exports|this|module.exports = require("...") (reexport)
  • exports|this|module.exports.xxx = require("...").xxx (reexport)
  • Object.defineProperty(exports|this|module.exports, "xxx", ...)
  • require("abc").xxx
  • require("abc").xxx()
  • 从 ESM 导入
  • require() 一个 ESM 模块
  • 被标记的导出类型 (对非严格 ESM 导入做特殊处理):
    • Object.defineProperty(exports|this|module.exports, "__esModule", { value: true|!0 })
    • exports|this|module.exports.__esModule = true|!0
  • 未来计划支持更多的构造

当检测到不可分析的代码时,webpack 会放弃,并且完全不跟踪这些模块的导出信息(出于性能考虑)。

开发与生产的一致性问题

我们试图通过改善两种模式的相似性,在开发模式的构建性能和避免仅在生产模式的产生的问题之间找到一个很好的平衡点。

Webpack 5 默认在两种模式下都启用了 “sideEffects “优化。在 webpack 4 中,由于 package.json 中的"sideEffects"标记不正确,这种优化导致了一些只在生产模式下出现的错误。在开发过程中启用这个优化可以更快更容易地发现这些问题。

在很多情况下,开发和生产都是在不同的操作系统上进行的,文件系统的大小写敏感度不同,所以 webpack 5 增加了一些奇怪的大小写的警告/错误。

改进 target 配置

在 webpack 4 中,”target “是在 "web""node" 之间的一个粗略的选择(还有一些其他的)。Webpack 5 给你更多的选择。target选项现在比以前影响了更多关于生成代码的事情,比如代码块加载方法、代码块的格式、externals 是否默认被启用等等。

此外,对于其中的一些情况,在 "web""node" 之间的选择过于粗略,我们需要更多的信息。因此,我们允许指定最低版本,例如 "node10.13",并推断出更多关于目标环境的属性。

现在也允许用一个数组组合多个目标,webpack 将确定所有目标的最小属性。使用数组也很有用,当使用像 "web""node" 这样没有提供完整信息的目标时(没有版本号)。例如,["web", "es2020"] 结合了这两个部分目标。

有一个目标 "browserslist",它将使用 browserslist 类库的数据来确定环境的属性。当项目中存在可用的 browserslist 配置时,这个目标也会被默认使用。当没有可用的配置时,默认使用 "web"目标。

代码块拆分与模块大小

现在模块的尺寸比单一的数字更好的表达方式。现在有不同类型的大小。

SplitChunksPlugin 现在知道如何处理这些不同的大小,并将它们用于 minSizemaxSize。默认情况下,只有 javascript 大小被处理,但你现在可以传递多个值来管理它们:

module.exports = {
  optimization: {
    splitChunks: {
      minSize: {
        javascript: 30000,
        webassembly: 50000,
      },
    },
  },
};

你仍然可以使用一个数字来表示大小。在这种情况下,webpack 会自动使用默认的大小类型。

mini-css-extract-plugin 使用 css/mini-extra 作为大小类型,并将此大小类型自动添加到默认类型中。

还有其它的一些构建优化,比如单个运行时的改进、模块合并、通用 Tree Shaking 改进、个别生成代码的改进、请参阅详情的 webpack 5 发布资讯。

性能优化

持久缓存

现在有一个文件系统缓存。它是可选的,可以通过以下配置启用:

module.exports = {
  cache: {
    // 1. 将缓存类型设置为文件系统
    type: 'filesystem',


    buildDependencies: {
      // 2. 将你的 config 添加为 buildDependency,以便在改变 config 时获得缓存无效
      config: [__filename],


      // 3. 如果你有其他的东西被构建依赖,你可以在这里添加它们
      // 注意,webpack、加载器和所有从你的配置中引用的模块都会被自动添加
    },
  },
};

重要说明:

默认情况下,webpack 假定 webpack 所在的 node_modules 目录只被包管理器修改。对 node_modules 来说,哈希值和时间戳会被跳过。出于性能考虑,只使用包名和版本。只要不指定resolve.symlinks: false,Symlinks(即npm/yarn link)就没有问题(无论如何都要避免)。不要直接编辑node_modules 中的文件,除非你用 snapshot.managedPaths: []以剔除该优化。当使用 Yarn PnP 时,webpack 假设 yarn 缓存是不可改变的(通常是这样)。你可以使用 snapshot.immutablePaths: [] 来退出这个优化。

缓存将默认存储在 node_modules/.cache/webpack(当使用 node_modules 时)或 .yarn/.cache/webpack(当使用 Yarn PnP 时)中。当所有的插件都正确处理缓存时,你可能永远都不需要手动删除它。

许多内部插件也会使用持久性缓存。例如 SourceMapDevToolPlugin (缓存 SourceMap 的生成)或ProgressPlugin (缓存模块数量)

持久性缓存将根据使用情况自动创建多个缓存文件,以优化对缓存的读写访问。

默认情况下,时间戳将用于开发模式的快照,而文件哈希将用于生产模式。文件哈希也允许在 CI 中使用持久性缓存。

编译器闲置和关闭

编译器现在需要在使用后关闭。编译器现在会进入和离开空闲状态,并且有这些状态的钩子。插件可能会使用这些钩子来做不重要的工作。(即将持久缓存缓慢地将缓存存储到磁盘上)。在编译器关闭时–所有剩余的工作应该尽可能快地完成。一个回调标志着关闭完成。

插件和它们各自的作者应该预料到,有些用户可能会忘记关闭编译器。所以,所有的工作最终也应该在空闲状态下完成。当工作正在进行时,应该防止进程退出。

webpack() 用法在被传递回调时自动调用close

迁移:在使用 Node.js API 时,一定要在完成工作后调用 Compiler.close

文件生成

webpack 过去总是在第一次构建时发出所有的输出文件,但在增量(观察)构建时跳过了写入未更改的文件。假设在 webpack 运行时,没有任何其他东西改变输出文件。

增加了持久性缓存后,即使在重启 webpack 进程时,也应该会有类似监听的体验,但如果认为即使在 webpack 不运行时也没有其他东西改变输出目录,那这个假设就太强了。

所以 webpack 现在会检查输出目录中现有的文件,并将其内容与内存中的输出文件进行比较。只有当文件被改变时,它才会写入文件。这只在第一次构建时进行。任何增量构建都会在运行中的 webpack 进程中生成新的资产时写入文件。

我们假设 webpack 和插件只有在内容被改变时才会生成新的资产。应该使用缓存来确保在输入相同时不会生成新的资产。不遵循这个建议会降低性能。

被标记为 [不可变] 的文件(包括内容哈希),当已经存在一个同名文件时,将永远不会被写入。我们假设当文件内容发生变化时,内容哈希会发生变化。这在一般情况下是正确的,但在 webpack 或插件开发过程中可能并不总是如此。

重大变更:长期未解决的问题

单一文件目标的代码分割

只允许启动单个文件的目标(如 node、WebWorker、electron main)现在支持运行时自动加载引导所需的依赖代码片段。

这允许对这些目标使用 chunks: "all"optimization.runtimeChunk

请注意,如果目标的代码块加载是异步的,这使得初始评估也是异步的。当使用 output.library时,这可能是一个问题,因为现在导出的值是一个 Promise。

更新了解析器

enhanced-resolve 更新到了 v5,有以下改进:

  • 追踪更多的依赖关系,比如丢失的文件。
  • 别名可能有多种选择
  • 现在可以别名为 false 了。
  • 支持 exportsimports 字段等功能。
  • 性能提高

没有 JS 的代码块

不包含 JS 代码的块,将不再生成 JS 文件。这就允许有只包含 CSS 的代码块。

重大变更:未来计划

实验特性

在 webpack 5 中,有一个新的 experiments 配置选项,允许启用实验性功能。这使得哪些功能被启用/使用变得很清楚。

虽然 webpack 遵循语义版本化,但它会对实验性功能进行例外处理。实验性功能可能会在 webpack 的次要版本中包含破坏性的变化。当这种情况发生时,我们会在变更日志中添加一个明确的注释。这将使我们能够更快地迭代实验性功能,同时也使我们能够在主要版本上为稳定的功能停留更长时间。

以下的实验功能将随 webpack 5 一起发布。

  • 旧的 WebAssembly 支持,就像 webpack 4 一样 (experiments.syncWebAssembly)
  • 根据更新的规范(experiments.asyncWebAssembly),新增 WebAssembly 支持。
    • 这使得一个 WebAssembly 模块成为一个异步模块。
  • 顶层的 Await第三阶段提案(experiments.topLevelAwait)
    • 在顶层使用 await 使该模块成为一个异步模块。
  • 以模块的形式生成代码包 (experiments.outputModule)
    • 这就从代码包中移除了包装器 IIFE,执行严格模式,通过 “ 进行懒惰加载,并在模块模式下最小化压缩。

请注意,这也意味着 WebAssembly 的支持现在被默认禁用。

最小 Node.js 版本

最低支持的 Node.js 版本从 6 增加到 10.13.0(LTS)。

迁移:升级到最新的 Node.js 版本。

主要的内部架构变更

这部分内容主要是那些想贡献 webpack 内核,以及加载器、插件开发者需要密切关注的。如果你只是使用 webpack,可以忽略这部分。内容非常多,而且比较难懂。

以下咱们来介绍一些最主要的一些内部架构的变更。

新的插件运行顺序

现在 webpack 5 中的插件在应用配置默认值之前就会被应用。这使得插件可以应用自己的默认值,或者作为配置预设。但这也是一个突破性的变化,因为插件在应用时不能依赖配置值的设置。

迁移:只在插件钩子中访问配置。或者最好完全避免访问配置,并通过构造函数获取选项。

运行时模块

大部分的运行时代码被移到了所谓的”运行时模块”中。这些特殊模块负责添加运行时代码。它们可以被添加到任何块中,但目前总是被添加到运行时块中。”运行时需求”控制哪些运行时模块(或核心运行时部件)被添加到代码包中。这确保了只有使用的运行时代码才会被添加到代码包中。未来,运行时模块也可以添加到按需加载的块中,以便在需要时加载运行时代码。

在大多数情况下,核心运行代码时允许内联入口模块,而不是用 __webpack_require__ 来调用它。如果代码包中没有其他模块,则根本不需要使用__webpack_require__。这与模块合并很好地结合在一起,即多个模块被合并成一个模块。在最好的情况下,根本不需要运行时代码。

迁移:如果你在插件中注入运行时代码到 webpack 运行时,可以考虑使用 RuntimeModules 来代替。

序列化

我们添加了一个序列化机制,以允许在 webpack 中对复杂对象进行序列化。它有一个可选的语义,所以那些应该被序列化的类需要被明确地标记出来(并且实现它们的序列化)。大多数模块、所有的依赖关系和一些错误都已经这样做了。

迁移:当使用自定义模块或依赖关系时,建议将它们实现成可序列化的,以便从持久化缓存中获益。

用于缓存的插件

增加了一个带有插件接口的 Cache 类。该类可用于写入和读取缓存。根据配置的不同,不同的插件可以为缓存添加功能。MemoryCachePlugin 增加了内存缓存功能。FileCachePlugin 增加了持久性(文件系统)缓存。FileCachePlugin 使用序列化机制将缓存项目持久化到磁盘上或从磁盘上恢复。

Tapable 插件升级

webpack 3 插件的 compat 层已经被移除。它在 webpack 4 中已经被取消了。一些较少使用的 tapable API 被删除或废弃。

迁移:使用新的 tapable API。

Main/Chunk/ModuleTemplate 废弃

打包模板已经重构。MainTemplate/ChunkTemplate/ModuleTemplate 被废弃,现在 JavascriptModulesPlugin 负责 JS 模板。

在那次重构之前,JS 输出由 Main/ChunkTemplate 处理,而另一个输出(即 WASM、CSS)则由插件处理。这样看起来 JS 是一等公民,而其它输出是二等。重构改变了这一点,所有的输出都由他们的插件处理。

依然可以侵入部分模板。钩子现在在 JavascriptModulesPlugin 中,而不是 Main/ChunkTemplate 中。(是的,插件也可以有钩子,我称之为附加钩子。)有一个兼容层,所以 Main/Chunk/ModuleTemplate 仍然存在,但只是将 tap 调用委托给新的钩子位置。

迁移:按照 deprecation 消息中的建议。主要是指向不同位置的钩子。

入口文件的新增配置

在 webpack 5 中,入口文件除了字符串、字符串数组,也可以使用描述符进行配置了,如:

module.exports = {
  entry: {
    catalog: {
      import: './catalog.js',
    },
  },
};

此外,也可以定义输出的文件名,之前都是通过 output.filename 进行定义的:

module.exports = {
  entry: {
    about: { import: './about.js', filename: 'pages/[name][ext]' },
  },
};

另外,入口文件的配置,新增了文件依赖定义、生成类库的格式类型(commonjs 或 amd),也可以设置运行时的名字,以及代码块加载的方式,更多细节可以参考完整的发布记录。

排序与 ID

webpack 曾经在编译阶段以特定的方式对模块和代码块进行排序,以递增的方式分配 ID。现在不再是这样了。顺序将不再用于 ID 的生成,取而代之的是,ID 生成的完全控制在插件中。优化模块和代码块顺序的钩子已经被移除。

迁移:在编译阶段,你不能再依赖模块和代码块的顺序了。

从数组到集合(Set)

  • Compilation.modules 现在是一个集合
  • Compilation.chunks 现在是一个集合
  • Chunk.files 现在是一个集合

存在一个适配层但会打印废弃的警告。

迁移: 使用集合方法代替数组方法。

文件系统与信息变更

webpack 5 中,一个是需要使用 Compilation.fileSystemInfo 替代file/contextTimestamps,获取文件的时间戳信息,另一个是新增Compiler.modifiedFiles 以便更容易引用更改后的文件。

另外,还新增了一个类似于 compiler.inputFileSystemcompiler.outputFileSystem 的新 API compiler.intermediateFileSystem,用于所有不被认为是输入或输出的 fs 操作,如写入 records,缓存或输出 profiling。

模块热替换

HMR 运行时已被重构为运行时模块。HotUpdateChunkTemplate 已被合并入 ChunkTemplate中。ChunkTemplates 和 plugins 也应处理 HotUpdateChunk 了。

HMR 运行时的 javascript 部分已从核心 HMR 运行时钟分离了出来。其他模块类型现在也可以使用它们自己的方式处理 HMR。在未来,这将使得 HMR 处理诸如 mini-css-extract-plugin 或 WASM 模块。

迁移:此为新功能,无需迁移。

import.meta.webpackHot 公开了与 module.hot 相同的 API。当然可以在 ESM 模块(.mjs,package.json 中的 type: “module”)中使用,这些模块不能访问 module

工作队列

webpack 曾经通过函数调用函数的形式来进行模块处理,还有一个 semaphore 选项限制并行性。Compilation.semaphore 已被移除,现在可以使用异步队列处理,每个步骤都有独立的队列:

  • Compilation.factorizeQueue:为一组 dependencies 调用模块工厂。
  • Compilation.addModuleQueue:将模块添加到编译队列中(可以使用缓存恢复模块)
  • Compilation.buildQueue:必要时构建模块(可将模块存储到缓存中)
  • Compilation.rebuildQueue:如需手动触发,则会重新构建模块
  • Compilation.processDependenciesQueue:处理模块的 dependencies。

这些队列会有一些 hook 来监听并拦截工作的进程。未来,多个编译器会同时工作,可以通过拦截这些队列来进行编译工作的编排。

迁移:此为新功能,无需迁移。

模块和 chunk 图

webpack 曾经在依赖关系中存储了已解析的模块,并在 chunk 中存储引入的模块。但现已发生变化。所有关于模块在模块图中如何连接的信息,现在都存储在 ModulGraph 的 class 中。所有关于模块与 chunk 如何连接的信息现在都已存储在 ChunkGraph 的 class 中。依赖于 chunk 图的信息也存储在相关的 class 中。

以下列举一些模块的信息已被移动的例子:

  • Module connections -> ModuleGraph
  • Module issuer -> ModuleGraph
  • Module optimization bailout -> ModuleGraph (TODO: check if it should ChunkGraph instead)

当从缓存中恢复模块时,webpack 会将模块从图中断开。现在已无需这么做。一个模块不存储图形的任何信息,技术上可以在多个图形中使用。这会使得缓存变得更加容易。这部分变化中大多数都有一个适配层,当使用时,它会打印一个弃用警告。

迁移:在 ModuleGraph 和 ChunkGraph 上使用新的 API。

模块 Source Types

Modules 现在必须通过 Module.getSourceTypes() 来定义它们支持的源码类型。根据这一点,不同的插件会用这些类型调用 source()。对于源类型为 javascriptJavascriptModulesPlugin 会将源代码嵌入到 bundle 中。源类型 webassemblyWebAssemblyModulesPlugin 会 emit 一个 wasm 文件。同时,也支持自定义源类型,例如,mini-css-extract-plugin 会使用源类型为 stylesheet 将源码嵌入到 css 文件中。

模块类型与源类型间没有关系。即使模块类型为 json,也可以使用源类型为 javascript 和模块类型为 webassembly/experimentaljavascriptwebassembly

迁移:自定义模块需要实现这些新的接口方法。

全新的观察者

webpack 所使用的观察者已重构。它之前使用的是 chokidar 和原生依赖 fsevents(仅在 OSX 上)。现在它在只基于原生的 Node.js 中的 fs。这意味着在 webpack 中已经没有原生依赖了。

它还能在监听时捕捉更多关于文件系统的信息。目前,它还可以捕获 mtimes 和监视事件时间,以及丢失文件的信息。为此,WatchFileSystem API 做了一点小改动。在修改的同时,我们还将 Arrays 转换为 Sets,Objects 转换为 Maps。

SizeOnlySource after emit

webpack 现在使用 SizeOnlySource 替换 Compilation.assets 中的 Sources,以减少内存占用。

ExportsInfo

重构了模块导出信息的存储方式。ModuleGraph 现在为每个 Module 提供了一个 ExportsInfo,它用于存储每个 export 的信息。如果模块仅以副作用的方式使用,它还存储了关于未知 export 的信息,

对于每个 export,都会存储以下信息:

  • 是否使用 export? 是否使用并不确定。(详见 optimization.usedExports
  • 是否提供 export? 是否提供并不确定。(详见 optimization.providedExports
  • 能否重命名 export 名? 是否重命名,也不确定
  • 如果 export 已重新命名,则为新名称。(详见 optimization.mangleExports
  • 嵌套的 ExportsInfo,如果 export 是一个含有附加信息的对象,那么它本身就是一个对象
    • 用于重新导出命名空间对象:import * as X from "..."; export { X };
    • 用于表示 JSON 模块中的结构

代码生成阶段

编译的代码生成功能作为单独的编译阶段。它不再隐藏在 Module.source()Module.getRuntimeRequirements() 中运行了。这应该会使得流程更加简洁。它还运行报告该阶段的进度。并使得代码生成在剖析时更加清晰可见。

迁移:Module.source()Module.getRuntimeRequirements() 已弃用。使用 Module.codeGeneration() 代替。

依赖关系参考

webpack 曾经有一个单一的方法和类型来表示依赖关系的引用(Compilation.getDependencyReference 会返回一个 DependencyReference)该类型用于引入关于该引用的所有信息,如 被引用的模块,已经引入了哪些 export,如果是弱引用,还需要订阅一些相关信息。把所有这些信息构建在一起,拿到参考的成本就很高,而且很频繁(每次有人需要一个信息)。

在 webpack5 中,这部分代码库被重构了,方法进行了拆分。

  • 引用的模块可以从 ModuleGraphConnection 中读取
  • 引入的导出名,可以通过 Dependency.getReferencedExports() 获取
  • Dependency 的 class 上会有一个 weak 的 flag
  • 排序只与 HarmonyImportDependencies 相关,可以通过 sourceOrder 属性获取

Presentational Dependencies

这是 NormalModules 的一种新 Dependencies 类型:Presentational Dependencies

这些 dependencies 只在代码生成阶段使用,但在模块图构建过程中未使用。所以它们永远不能引用模块或影响导出/导入。

这些依赖关系的处理成本较低,webpack 会尽可能地使用它们

弃用 loaders

  • null-loader

已被弃用。使用

  module.exports = {
    resolve: {
      alias: {
        xyz$: false,
      },
    },
  };

或者使用绝对路径

  module.exports = {
    resolve: {
      alias: {
        [path.resolve(__dirname, '....')]: false,
      },
    },
  };

总结

webpack 5 的大部分工作围绕优化展开,去除了 4 中有废弃的内容,新增了长期缓存,优化了内核等。本文只是挑重点为大家说明,具体变更请大家参考官方文档。

官网:https://webpack.js.org 中文文档:https://webapck.docschina.org

以上就是W3Cschool编程狮关于阔别两年,webpack 5 正式发布了!的相关介绍了,希望对大家有所帮助。

初中级前端必须要知道的JS数据类型

thbcm阅读(190)

JavaScript中有哪些数据类型?

计算机世界中定义的数据类型其实就是为了描述现实世界中存在的事实而定义的。比如我们用人来举例:

  1. 有没有人在房间里?这里的有和没有就是是或者非的概念,在 JS 中对应 Boolean 类型, true 表示是, false 表示非;
  2. 有几个人在房间里?这里的几个表示的是一个量级概念,在 JS 中对应 Number 类型,包含整数和浮点数,还有一些特殊的值,比如: -Infinity 表示负无穷大、 +Infinity 表示正无穷大、 NaN 表示不是一个数字;
  3. 房间里的这些人都是我的朋友。这是一句陈述语句,这种文本类的信息将会以字符串形式进行存储,在 JS 中对应 String 类型;
  4. 房间里没有人。这里的没有代表无和空的概念,在 JSnullundefined 都可以表示这个意思;
  5. 现实世界中所有人都是独一无二的,这在 JS 中对应 Symbol 类型,表示唯一且不可改变;
  6. Number 所表示的整数是有范围的,超出范围的数据就没法用 Number 表示了,于是 ES10 中提出了一种新的数据类型 BigInt,能表示任何位数的整数;
  7. 以上提到的 BooleanNumberStringnullundefinedSymbolBigInt 等7种类型都是 JavaScript 中的原始类型,还有一种是非原始类型叫做对象类型;比如:一个人是对象,这个人有名字、性别、年龄等;
let person = {
    name: 'bubuzou',
    sex: 'male',
    age: 26,}

为什么要区分原始类型和对象类型?他们之间有什么区别?

原始类型的不可变性

在回答这个问题之前,我们先看一下变量在内存中是如何存储的:

let name1 = 'bubuzou'
let name2 = name1.concat('.com')
console.log(name1)  // 'bubuzou'

执行完上面这段代码,我们发现变量 name1 的值还是不变,依然是 bubuzou。这就说明了字符串的不可变性。但是你看了下面的这段代码,你就会产生疑问了:

let name1 = 'bubuzou'
name1 += '.com'
console.log(name1)  // 'bubuzou.com'

你说字符串是不可变的,那现在不是变了嘛? 其实这只是变量的值变了,但是存在内存中的字符串依然不变。这就涉及到变量在内存中的存储了。 在 JavaScript 中,变量在内存中有2种存储方式:存在栈中和存在堆中。那么栈内存和堆内存有啥区别呢?

栈内存:

  • 顺序存储结构,特点是先进后出。就像一个兵乒球盒子一样,兵乒球从外面一个个的放入盒子里,最先取出来的一定是最后放入盒子的那个。
  • 存储空间固定
  • 可以直接操作其保存的值,执行效率高

堆内存:

  • 无序的存储结构
  • 存储空间可以动态变化
  • 无法直接操作其内部的存储,需要通过引用地址操作

了解完变量在内存中的存储方式有2种,那我们继续以上面那串代码为例,画出变量的存储结构图:

然后我们可以描述下当计算机执行这段代码时候的发生了什么?首先定义了一个变量 name1 并且给其赋值 bubuzou 这个时候就会在内存中开辟一块空间用来存储字符串 bubuzou,然后变量指向了这个内存空间。然后再执行第二行代码 letname2=name1.concat('.com') 这里的拼接操作其实是产生了一个新字符串 bubuzou.com,所以又会为这个新字符串创建一块新内存,并且把定义的变量 name2 指向这个内存地址。 所以我们看到其实整个操作 bubuzou 这个字符串所在的内存其实是没有变化的,即使在第二段代码中执行了 name1+='.com' 操作,其实也只是变量 name1 指向了新的字符串 bubuzou.com 而已,旧的字符串 bubuzou 依然存在内存中,不过一段时间后由于该字符串没有被变量所引用,所以会被当成垃圾进行回收,从而释放掉该块内存空间。

从而我们得出结论:原始类型的值都是固定的,而对象类型则是由原始类型的键值对组合成一个复杂的对象;他们在内存中的存储方式是不一样的,原始类型的值直接存在栈内存中,而对象类型的实际值是存在堆内存中的,在栈内存中保存了一份引用地址,这个地址指向堆内存中的实际值,所以对象类型又习惯被叫做引用类型。

想一个问题为什么引用类型的值要存储到堆内存中?能不能存到栈内存中呢?答案一:因为引用类型大小不固定,而栈的大小是固定的,堆空间的大小是可以动态变化的,所以引用类型的值适合存在堆中;答案二:在代码执行过程中需要频繁的切换执行上下文的时候,如果把引用类型的值存到栈中,将会造成非常大的内存开销。

比较

当我们对两个变量进行比较的时候,不同类型的变量是有不同表现的:

let str1 = 'hello'
let str2 = 'hello'
console.log( str1 === str2 ) // true
let person1 = {
    name: 'bubuzou'
}
let person2 = {
    name: 'bubuzou'
}
console.log( person1 === person2 )  // false

我们定义了2个字符串变量和2个对象变量,他们都长一模一样,但是字符串变量会相等,对象变量却不相等。这是因为在 JavaScript 中,原型类型进行比较的时候比较的是存在栈中的值是否相等;而引用类型进行比较的时候,是比较栈内存中的引用地址是否相等。 如上几个变量在内存中的存储模型如图所示:

复制

变量进行复制的时候,原始类型和引用类型变量也是有区别的,来看下面的代码:

let str1 = 'hello'
let str2 = str1
str2 = 'world'
console.log( str1 ) // 'hello'

  1. letstr1='hello': 复制前,定义了一个变量 str1,并且给其赋值 hello,这个时候 hello 这个字符串就会在栈内存中被分配一块空间进行存储,然后变量 str1 会指向这个内存地址;
  2. letstr2=str1:复制后,把 str1 的值赋值给 str2,这个时候会在栈中新开辟一块空间用来存储 str2 的值;
  3. str2='world':给 str2 赋值了一个新的字符串 world,那么将新建一块内存用来存储 world,同时 str2 原来的值 hello 的内存空间因为没有变量所引用,所以一段时间后将被当成垃圾回收;
  4. console.log(str1):因为 str1str2 的栈内存地址是不一样的,所以即使 str2 的值被改变,也不会影响到 str1

然后我们继续往下,看下引用类型的复制:

let person1 = {
    name: 'bubuzou',
    age: 20
}
let person2 = person1
person2.name = 'bubuzou.com'
console.log( person1.name)  // 'bubuzou.com'

原始类型进行复制的时候是变量的值进行重新赋值,而如上图所示:引用类型进行复制的时候是把变量所指向的引用地址进行赋值给新的变量,所以复制后 person1person2 都指向堆内存中的同一个值,所以当改变 person2.name 的时候, person1.name 也会被改变就是这个原因。

值传递和引用传递

先说一下结论,在 JavaScript 中,所有函数的参数传递都是按值进行传递的。看如下代码:

let name = 'bubuzou'
function changeName(name) {
    name = 'bubuzou.com'
}
changeName(name)
console.log( name )  // 'bubuzou'

定义了一个变量 name,并赋值为 bubuzou,函数调用的时候传入 name,这个时候会在函数内部创建一个局部变量 name 并且把全局变量的值 bubuzou 传递给他,这个操作其实是在内存里新建了一块空间用来存放局部变量的值,然后又把局部变量的值改成了 bubuzou.com,这个时候其实内存中会有3块地址空间分别用来存放全局变量的值 bubuzou、局部变量原来的值 bubuzou、和局部变量新的值 bubuzou.com;一旦函数调用结束,局部变量将被销毁,一段时间后由于局部变量新旧值没有变量引用,那这两块空间将被回收释放;所以这个时候全局 name 的值依然是 bubuzou

再来看看引用类型的传参,会不会有所不同呢?

let person = {
    name: 'bubuzou'
}
function changePerosn(person) {
    person.name = 'bubuzou.com'
}
changePerosn( person )
console.log( person.name )  // 'bubuzou.com'

引用类型进行函数传参的时候,会把引用地址复制给局部变量,所以全局的 person 和函数内部的局部变量 person 是指向同一个堆地址的,所以一旦一方改变,另一方也将被改变,所以至此我们是不是可以下结论说:当函数进行传参的时候如果参数是引用类型那么就是引用传递嘛?

将上面的例子改造下:

let person = {
    name: 'bubuzou'
}
function changePerosn(person) {
    person.name = 'bubuzou.com'
    person = {        name: 'hello world'    
    }
}
changePerosn( person )
console.log( person.name )  // 'bubuzou.com'

如果 person 是引用传递的话,那就会自动指向值被改为 hello world 的新对象;事实上全局变量 person 的引用地址自始至终都没有改变,倒是局部变量 person 的引用地址发生了改变。

null 和 undefined 傻傻分不清?

nullJavaScript 中自成一种原始类型,只有一个值 null,表示无、空、值未知等特殊值。可以直接给一个变量赋值为 null

let s = null

undefinednull 一样也是自成一种原始类型,表示定义了一个变量,但是没有赋值,则这个变量的值就是 undefined:

let s
console.log( s)  // undefined

虽然可以给变量直接赋值为 undefined 也不会报错,但是原则上如果一个变量值未定,或者表示空,则直接赋值为 null 比较合适,不建议给变量赋值 undefinednullundefined 在进行逻辑判断的时候都是会返回 false 的:

let a = null, b
console.log( a ? 'a' : b ? 'b' : 'c') // 'c'

null 在转成数字类型的时候会变成 0,而 undefined 会变成 NaN:

let a = null, b
console.log( +null )  // 0
console.log( + b )  // NaN

认识新的原始类型 Symbol

Symbol 值表示唯一标识符,是 ES6 中新引进的一种原始类型。可以通过 Symbol() 来创建一个重要的值,也可以传入描述值;其唯一性体现在即使是传入一样的描述,他们两者之间也是不会相等的:

let a = Symbol('bubuzou')
let b = Symbol('bubuzou')
console.log( a === b )  // false

全局的 Symbol

那还是不是任意2个描述一样的 Symbol 都是不相等的呢?答案是否定的。可以通过 Symbol.for() 来查找或新建一个 Symbol

let a = Symbol.for('bubuzou')
let b = Symbol.for('bubuzou')
console.log( a === b )  // true

使用 Symbol.for() 可以在根据传入的描述在全局范围内进行查找,如果没找到则新建一个 Symbol,并且返回;所以当执行第二行代码 Symbol.for('bubuzou') 的时候,就会找到全局的那个描述为 bubuzouSymbol,所以这里 ab 是会绝对相等的。

居然可以通过描述找到 Symbol, 那是否可以通过 Symbol 来找到描述呢?答案是肯定的,但是必须是全局的 Symbol,如果没找到则会返回 undefined:

let a = Symbol.for('bubuzou')
let desc = Symbol.keyFor( a )
console.log( desc )  // 'bubuzou'

但是对于任何一个 Symbol 都有一个属性 description,表示这个 Symbol 的描述:

let a = Symbol('bubuzou')
console.log( a.description )  // 'bubuzou'

Symbol 作为对象属性

我们知道对象的属性键可以是字符串,但是不能是 Number 或者 BooleanSymbol 被设计出来其实最大的初衷就是用于对象的属性键:

let age = Symbol('20')
let person = {
    name: 'bubuzou', 
    [age]: '20',  // 在对象字面量中使用 `Symbol` 的时候需要使用中括号包起来
}

这里给 person 定义了一个 Symbol 作为属性键的属性,这个相比于用字符串作为属性键有啥好处呢?最明显的好处就是如果这个 person 对象是多个开发者进行开发维护,那么很容易再给 person 添加属性的时候出现同名的,如果是用字符串作为属性键那肯定是冲突了,但是如果用 Symbol 作为属性键,就不会存在这个问题了,因为它是唯一标识符,所以可以使对象的属性受到保护,不会被意外的访问或者重写。

注意一点,如果用 Symbol 作为对象的属性键的时候, forinObject.getOwnPropertyNames、或 Object.keys() 这里循环是无法获取 Symbol 属性键的,但是可以通过 Object.getOwnPropertySymbols() 来获取;在上面的代码基础上:

for (let o in person) {
    console.log( o ) // 'name'
}
console.log (Object.keys( person )) // ['name']
console.log(Object.getOwnPropertyNames( person ))  // ['name']
console.log(Object.getOwnPropertySymbols( person ))  // [Symbol(20)]

你可能不知道的 Number 类型

JavaScript 中的数字涉及到了两种类型:一种是 Number 类型,以 64 位的格式 IEEE-754 存储,也被称为双精度浮点数,就是我们平常使用的数字,其范围是 $2^{52}$ 到 -$2^{52}$;第二种类型是 BigInt,能够表示任意长度的整数,包括超出 $2^{52}$ 到 -$2^{52}$ 这个范围外的数。这里我们只介绍 Number 数字。

常规数字和特殊数字

对于一个常规的数字,我们直接写即可,比如:

let age = 20

但是还有一种位数特别多的数字我们习惯用科学计数法的表示方法来写:

let billion = 1000000000;
let b = 1e9

以上两种写法是一个意思, 1e9 表示 1 x $10^9$;如果是 1e-3 表示 1 / $10^3$ = 0.001。 在 JavaScript 中也可以用数字表示不同的进制,比如:十进制中的 10 在 二、八和十六进制中可以分别表示成 0b10100o120xa;其中的 0b 是二进制前缀, 0o 是八进制前缀,而 ox 是十六进制的前缀。

我们也可以通过 toString(base) 方法来进行进制之间的转换, base 是进制的基数,表示几进制,默认是 10 进制的,会返回一个转换数值的字符串表示。比如:

let num = 10
console.log( num.toString( 2 ))  // '1010'
console.log( num.toString( 8 ))  // '12'
console.log( num.toString( 16 ))  // 'a'

数字也可以直接调用方法, 10..toString(2) 这里的 2个 . 号不是写错了,而是必须是2个,否则会报 SyntaxError 错误。第一个点表示小数点,第二个才是调用方法。点符号首先会被认为是数字常量的一部分,其次再被认为是属性访问符,如果只写一个点的话,计算机无法知道这个是表示一个小数呢还是去调用函数。数字直接调用函数还可以有以下几种写法:

(10).toString(2)  // 将10用括号包起来
10.0.toString(2)  // 将10写成10.0的形式
10 .toString(2)   // 空格加上点符号调用

Number 类型除了常规数字之外,还包含了一些特殊的数字:

  • NaN:表示不是一个数字,通常是由不合理的计算导致的结果,比如数字除以字符串 1/'a'; NaN 和任何数进行比较都是返回 false,包括他自己: NaN==NaN 会返回 false; 如何判断一个数是不是 NaN 呢?有四种方法:

方法一:通过 isNaN() 函数,这个方法会对传入的字符串也返回 true,所以判断不准确,不推荐使用:

isNaN( 1 / 'a')`  // true
isNaN( 'a' )  // true

方法二:通过 Number.isNaN(),推荐使用:

Number.isNaN( 1 / 'a')`  // true
Number.isNaN( 'a' )  // false

方法三:通过 Object.is(a,isNaN):

Object.is( 0/'a', NaN) // true
Object.is( 'a', NaN) // false

方法四:通过判断 n!==n,返回 true, 则 nNaN :

let s = 1/'a'
console.log( s !== s )  // true

  • +Infinity:表示正无穷大,比如 1/0 计算的结果, -Infinity 表示负无穷大,比如 -1/0 的结果。
  • +0-0JavaScript 中的数字都有正负之分,包括零也是这样,他们会绝对相等:
console.log( +0 === -0 )  // true

为什么 0.1 + 0.2 不等于 0.3

console.log( 0.1 + 0.2 == 0.3 )  // false

有没有想过为什么上面的会不相等?因为数字在 JavaScript 内部是用二进制进行存储的,其遵循 IEEE754 标准的,用 64 位来存储一个数字, 64 位又被分隔成 11152 位来分别表示符号位、指数位和尾数位。

比如十进制的 0.1 转成二进制后是多少?我们手动计算一下,十进制小数转二进制小数的规则是“乘2取整,顺序排列”,具体做法是:用2乘十进制小数,可以得到积,将积的整数部分取出,再用2乘余下的小数 部分,又得到一个积,再将积的整数部分取出,如此进行,直到积中的小数部分为零,或者达到所要求的精度为止。

0.1 * 2 = 0.2  // 第1步:整数为0,小数0.2
0.2 * 2 = 0.4  // 第2步:整数为0,小数0.4
0.4 * 2 = 0.8  // 第3步:整数为0,小数0.8
0.8 * 2 = 1.6  // 第4步:整数为1,小数0.6
0.6 * 2 = 1.2  // 第5步:整数为1,小数0.2
0.2 * 2 = 0.4  // 第6步:整数为0,小数0.4
0.4 * 2 = 0.8  // 第7步:整数为0,小数0.8...

我们这样依次计算下去之后发现得到整数的顺序排列是 0001100110011001100.... 无限循环,所以理论上十进制的 0.1 转成二进制后会是一个无限小数 0.0001100110011001100...,用科学计数法表示后将是 1.100110011001100... x $2^{-4}$ ,但是由于 IEEE754 标准规定了一个数字的存储位数只能是 64 位,有效位数是 52 位,所以将会对 1100110011001100.... 这个无限数字进行舍入总共 52 位作为有效位,然后二进制的末尾取舍规则是看后一位数如果是 1 则进位,如果是 0 则直接舍去。那么由于 1100110011001100.... 这串数字的第 53 位刚好是 1 ,所以最终的会得到的数字是 1100110011001100110011001100110011001100110011001101,即 1.100110011001100110011001100110011001100110011001101 x $2^{-4}$。 十进制转二进制也可以用 toString 来进行转化:

console.log( 0.1.toString(2) )  // '0.0001100110011001100110011001100110011001100110011001101'

我们发现十进制的 0.1 在转化成二进制小数的时候发生了精度的丢失,由于进位,它比真实的值更大了。而 0.2 其实也有这样的问题,也会发生精度的丢失,所以实际上 0.1+0.2 不会等于 0.3:

console.log( 0.1 + 0.2 )  // 0.30000000000000004

那是不是没办法判断两个小数是否相等了呢?答案肯定是否定的,想要判断2个小数 n1n2 是否相等可以如下操作:

  • 方法一:两小数之差的绝对值如果比 Number.EPSILON 还小,那么说明两数是相等的。

Number.EPSILONES6 中的误差精度,实际值可以认为等于 $2^{-52}$。

if ( Math.abs( n1 - n2 ) < Number.EPSILON ) {
    console.log( 'n1 和 n2 相等' )
}

  • 方法二:通过 toFixed(n) 对结果进行舍入, toFixed() 将会返回字符串,我们可以用 一元加 + 将其转成数字:
let sum = 0.1 + 0.2
console.log( +sum.toFixed(2) === 0.3 )  // true

数值的转化

对数字进行操作的时候将常常遇到数值的舍入和字符串转数字的问题,这里我们巩固下基础。先来看舍入的:

  • Math.floor(),向下舍入,得到一个整数:
Math.floor(2.2)  // 2
Math.floor(2.8)  // 2

  • Math.ceil(),向上舍入,得到一个整数:
Math.ceil(2.2)  // 3
Math.ceil(2.8)  // 3

  • Math.round(),对第一位小数进行四舍五入:
Math.round(2.26)  // 2
Math.round(2.46)  // 2
Math.round(2.50)  // 3

  • Number.prototype.toFixed(n),和 Math.round() 一样会进行四舍五入,将数字舍入到小数点后 n 位,并且以字符串的形式返回:
12..toFixed(2)  // '12.00'
12.14.toFixed(1)  // '12.1'
12.15.toFixed(1)  // '12.2'

为什么 6.35.toFixed(1) 会等于 6.3 ?因为 6.35 其实是一个无限小数:

6.35.toFixed(20)  // "6.34999999999999964473"

所以在 6.35.toFixed(1) 求值的时候会得到 6.3

再来看看字符串转数字的情况:

  • Number(n)+n,直接将 n 进行严格转化:
Number(' ')  // 0
console.log( +'') // 0
Number('010')  // 10
console.log( +'010' )  // 10
Number('12a')  // NaN
console.log( +'12a' )  // NaN

  • parseInt(),非严格转化,从左到右解析字符串,遇到非数字就停止解析,并且把解析的数字返回:
parseInt('12a')  // 12
parseInt('a12')  // NaN
parseInt('')  // NaN
parseInt('0xA')  // 10,0x开头的将会被当成十六进制数

parseInt() 默认是用十进制去解析字符串的,其实他是支持传入第二个参数的,表示要以多少进制的 基数去解析第一个参数:

parseInt('1010', 2)  // 10
parseInt('ff', 16)  // 255

如何判断一个数是不是整数?介绍两种方法:

  • 方法一:通过 Number.isInteger():
Number.isInteger(12.0)  // true
Number.isInteger(12.2)  // false

  • 方法二: typeofnum=='number'&&num%1==0
function isInteger(num) {
    return typeof num == 'number' && num % 1 == 0
}

引用类型

除了原始类型外,还有一个特别重要的类型:引用类型。高程里这样描述他:引用类型是一种数据结构, 用于将数据和功能组织在一起。到目前为止,我们看到最多的引用类型就是 Object,创建一个 Object 有两种方式:

  • 方式一:通过 new 操作符:
let person = new Object()
person.name = 'bubuzou'
person.age = 20

  • 方式二:通过对象字面量,这是我们最喜欢用的方式:
let person = {
    name: 'bubuzou',
    age: 20
}

内置的引用类型

除了 Object 外,在 JavaScript 中还有别的内置的引用类型,比如:

  • Array 数组
  • Date 日期
  • RegExp 正则表达式
  • Function 函数

他们的原型链的顶端都会指向 Object:

let d = new Date()
console.log( d.__proto__.__proto__.constructor )  // ƒ Object() { [native code] }

包装类型

先来看一个问题,为什么原始类型的变量没有属性和方法,但是却能够调用方法呢?

let str = 'bubuzou'
str.substring(0, 3)  // 'bub'

因为 JavaScript 为了更好地操作原始类型,设计出了几个对应的包装类型,他们分别是:

  • Boolean
  • Number
  • String

上面那串代码的执行过程其实是这样的:

  1. 创建 String 类型的一个实例;
  2. 在实例上调用指定的方法;
  3. 销毁这个实例

用代码体现一下:

let str = new
String('bubuzou')
str.substring(0, 3)
str = null

原始类型调用函数其实就是自动进行了装箱操作,将原始类型转成了包装类型,然后其实原始类型和包装类型是有本质区别的,原始类型是原始值,而包装类型是对象实例:

let str1 = 'bubuzou'
let str2 = new String('bubuzou')
console.log( str1 === str2 )  // fasle
console.log( typeof str1 )  // 'string'
console.log( typeof str2 )  // 'object'

居然有装箱操作,那肯定也有拆箱操作,所谓的拆箱就是包装类型转成原始类型的过程,又叫 ToPromitive,来看下面的例子:

let obj = {
    toString: () => { return 'bubuzou' },    
    valueOf: () => { return 20 },
}
console.log( +obj )  // 20
console.log( `${obj}` )  // 'bubuzou'

在拆箱操作的时候,默认会尝试调用包装类型的 toString()valueOf() 方法,对于不同的 hint 调用顺序会有所区别,如果 hintstring 则优先调用 toString(),否则的话,则优先调用 valueOf()。 默认情况下,一个 Object 对象具有 toString()valueOf() 方法:

let obj = {}
console.log( obj.toString() )  // '[object Object]'
console.log( obj.valueOf() )  // {},valueOf会返回对象本身

类型装换

Javascript 是弱类型的语音,所以对变量进行操作的时候经常会发生类型的转换,尤其是隐式类型转换,可能会让代码执行结果出乎意料之外,比如如下的代码你能理解其执行结果嘛?

[] + {}  // '[object Object]'
{} + []  // 0

类型转换规则

所以我们需要知道类型转换的规则,以下整理出一个表格,列出了常见值和类型以及转换之后的结果,仅供参考。

显示类型转换

我们平时写代码的时候应该尽量让写出来的代码通俗易懂,让别人能阅读后知道你是要做什么,所以在对类型进行判断的时候应该尽量显示的处理。 比如将字符串转成数字,可以这样:

Number( '21' )  // 21
Number( '21.8' )  // 21.8
+'21'  // 21 

将数字显示转成字符串可以这样:

String(21)  // '21'
21..toString()  // '21'

显示转成布尔类型可以这样:

Boolean('21')  // true
Boolean( undefined )  // false
!!NaN  // false
!!'21'  // true

除了以上之外,还有一些关于类型转换的冷门操作,有时候也挺管用的: 直接用一元加操作符获取当前时间的毫秒数:

+new Date()  // 1595517982686

~ 配合 indexOf() 将操作结果直接转成布尔类型:

let str = 'bubuzou.com'
if (~str.indexOf('.com')) {
    console.log( 'str如果包含了.com字符串,则会打印这句话' )
}

使用 ~~ 对字符或数字截取整数,和 Math.floor() 有稍许不同:

~~21.1  // 21
~~-21.9  // -21
~~'1.2a'  // 0
Math.floor( 21.1 )  // 21
Math.floor( -21.9 )  // -22

隐式类型转换

隐式类型转换发生在 JavaScript 的运行时,通常是由某些操作符或语句引起的,有下面这几种情况:

  • 隐式转成布尔类型:
  1. if(..)语句中的条件判断表达式。
  2. for(..;..;..)语句中的条件判断表达式(第二个)。
  3. while(..)do..while(..) 循环中的条件判断表达式。
  4. ?:中的条件判断表达式。
  5. 逻辑运算符 || (逻辑或)和 && (逻辑与)左边的操作数(作为条件判断表达式)
if (42) {
    console.log(42)
}
while ('bubuzou') {
    console.log('bubuzou')
}
const c = null ? '存在' : '不存在'  // '不存在'

上例中的非布尔值会被隐式强制类型转换为布尔值以便执行条件判断。 需要特别注意的是 ||&& 操作符。 || 的操作过程是只有当左边的值返回 false 的时候才会对右边进行求值且将它作为最后结果返回,类似 a?a:b 这种效果:

const a = 'a' || 'b'  // 'a'
const b = '' || 'c'  // 'c'

&& 的操作过程是只有当左边的值返回 true 的时候才对右边进行求值且将右边的值作为结果返回,类似 a?b:a 这种效果:

const a = 'a' && 'b'  // 'b'
const b = '' && 'c'  // ''

  • 数学操作符 -*/ 会对非数字类型的会优先转成数字类型,但是对 + 操作符会比较特殊:
  1. 当一侧为 String 类型,被识别为字符串拼接,并会优先将另一侧转换为字符串类型。
  2. 当一侧为 Number 类型,另一侧为原始类型,则将原始类型转换为 Number 类型。
  3. 当一侧为 Number 类型,另一侧为引用类型,将引用类型和 Number 类型转换成字符串后拼接。
42 + 'bubuzou'  // '42bubuzou'
42 + null  // 42
42 + true  // 43
42 + []  // '42'
42 + {}  // '42[object Object]'

  • 宽松相等和严格相等

宽松相等( ==)和严格相等( ===)在面试的时候经常会被问到,而回答一般是 == 是判断值是否相等,而 === 除了判断值会不会相等之外还会判断类型是否相等,这个答案不完全正确,更好的回答是: == 在比较过程中允许发生隐式类型转换,而 === 不会。 那 == 是怎么进行类型转换的呢?

1、 数字和字符串比,字符串将转成数字进行比较:

20 == '20'  // true
20 === '20'  // false

2、 别的类型和布尔类型比较,布尔类型将首先转成数字进行比较, true 转成数字 1, false 转成数字 0,注意这个是非常容易出错的一个点:

'bubuzou' == true  // false
'0' == false  // true
null == false  // false,
undefined == false  // false
[] == true  // false
['1']  == true  // true

所以写代码进行判断的时候一定不要写成 x==truex==false 这种,而应该直接 if(x) 判断。

3、 nullundefined: null==undefined 比较结果是 true,除此之外, nullundefined 和其他任何结果的比较值都为 false。可以认为在 == 的情况下, nullundefined 可以相互的进行隐式类型转换。

null == undefined // true
null == '' // false
null == 0 // false
null == false // false
undefined == '' // false
undefined == 0 // false
undefined == false // false

4、 原始类型和引用类型比较,引用类型会首先进行 ToPromitive 转成原始类型然后进行比较,规则参考上面介绍的拆箱操作:

'42'  == [42]  // true
'1,2,3'  == [1, 2, 3]  // true
'[object Object]' == {}  // true
0 == [undefined]  // true

5、 特殊的值

NaN == NaN  // false
+0 == -0  // true
[] == ![]  // true,![]的优先级比==高,所以![]先转成布尔值变成false;即变成[] == false,false再转成数字0,[]转成数字0,所以[] == ![]
0 == '\n'  // true

类型检测

用typeof检测原始类型

JavaScript 中有 nullundefinedbooleannumberstringSymbol 等六种原始类型,我们可以用 typeof 来判断值是什么原始类型的,会返回类型的字符串表示:

typeof undefined // 'undefined'
typeof true  // 'boolean'
typeof 42  // 'number'
typeof "42"  // 'string'
typeof Symbol()  // 'symbol'

但是原始类型中有一个例外, typeofnull 会得到 ‘object’,所以我们用 typeof 对原始值进行类型判断的时候不能得到一个准确的答案,那如何判断一个值是不是 null 类型的呢?

let o = null
!o && typeof o === 'object' // 用于判断 o 是否是 null 类型

undefinedundeclared 有什么区别?前者是表示在作用域中定义了但是没有赋值的变量,而后者是表示在作用域中没有定义的变量;分别表示 undefined 未定义、 undeclared 未声明。

typeof 能够对原始类型进行判断,那是否也能判断引用类型呢?

typeof []  // 'object'
typeof {}  // 'object'
typeof new Date()  // 'object'
typeof new RegExp()  // 'object'
typeof new Function()  // 'function'

从上面的结果我们可以得到这样一个结论: typeof 对引用类型判断的时候只有 function 类型可以正确判断,其他都无法正确判断具体是什么引用类型。

用instanceof检测引用类型

我们知道 typeof 只能对部分原始类型进行检测,对引用类型毫无办法。 JavaScript 提供了一个操作符 instanceof,我们来看下他是否能检测引用类型:

[] instanceof Array  // true
[] instanceof Object  // true 

我们发现数组即是 Array 的实例,也是 Object 的实例,因为所以引用类型原型链的终点都是 Object,所以 Array 自然是 Object 的实例。那么我们得出结论: instanceof 用于检测引用类型好像也不是很靠谱的选择。

用toString进行类型检测

我们可以使用 Object.prototype.toString.call() 来检测任何变量值的类型:

Object.prototype.toString.call(true)  // '[object Boolean]'
Object.prototype.toString.call(undefined)  // '[object Undefined]'
Object.prototype.toString.call(null)  // '[object Null]'
Object.prototype.toString.call(20)  // '[object Number]'
Object.prototype.toString.call('bubuzou')  // '[object String]'
Object.prototype.toString.call(Symbol())  // '[object Symbol]'
Object.prototype.toString.call([])  // '[object Array]'
Object.prototype.toString.call({})  // '[object Object]'
Object.prototype.toString.call(function(){})  // '[object Function]'
Object.prototype.toString.call(new Date())  // '[object Date]'
Object.prototype.toString.call(new RegExp())  // '[object RegExp]'
Object.prototype.toString.call(JSON)  // '[object JSON]'
Object.prototype.toString.call(MATH)  // '[object MATH]'
Object.prototype.toString.call(window)  // '[object RegExp]'

文章来源于公众号:大海我来了 ,作者布兰

以上就是W3Cschool编程狮关于初中级前端必须要知道的JS数据类型的相关介绍了,希望对大家有所帮助。

新手学习 react 迷惑的点(也可以复习,建议收藏!)

thbcm阅读(208)

网上各种言论说 React 上手比 Vue 难,可能难就难不能深刻理解 JSX,或者对 ES6 的一些特性理解得不够深刻,导致觉得有些点难以理解,然后说 React 比较难上手,还反人类啥的,所以我打算写两篇文章来讲新手学习 React 的时候容易迷惑的点写出来。

为什么要引入 React

在写 React 的时候,你可能会写类似这样的代码:

import React from 'react'


function A() {
  // ...other code
  return <h1>前端桃园</h1>
}

你肯定疑惑过,下面的代码都没有用到 React,为什么要引入 React 呢?

如果你把 import React from ‘react’ 删掉,还会报下面这样的错误:

那么究竟是哪里用到了这个 React,导致我们引入 React 会报错呢,不懂这个原因,那么就是 JSX 没有搞得太明白。

你可以讲上面的代码(忽略导入语句)放到在线 babel 里进行转化一下,发现 babel 会把上面的代码转化成:

function A() {
  // ...other code
  return React.createElement("h1", null, "前端桃园");
}

因为从本质上讲,JSX 只是为 React.createElement(component, props, ...children) 函数提供的语法糖。

为什么要用 className 而不用 class

  1. React 一开始的理念是想与浏览器的 DOM API 保持一直而不是 HTML,因为 JSX 是 JS 的扩展,而不是用来代替 HTML 的,这样会和元素的创建更为接近。在元素上设置 class 需要使用 className 这个 API:
   const element = document.createElement("div")
   element.className = "hello" 

  1. 浏览器问题,ES5 之前,在对象中不能使用保留字。以下代码在 IE8 中将会抛出错误:
   const element = {
    attributes: {
      class: "hello"
    }
   } 

  1. 解构问题,当你在解构属性的时候,如果分配一个 class 变量会出问题:
   const { class } = { class: 'foo' } // Uncaught SyntaxError: Unexpected token }
   const { className } = { className: 'foo' } 
   const { class: className } = { class: 'foo' } 

其他讨论可见:有趣的话题,为什么jsx用className而不是class

为什么属性要用小驼峰

因为 JSX 语法上更接近 JavaScript 而不是 HTML,所以 React DOM 使用camelCase(小驼峰命名)来定义属性的名称,而不使用 HTML 属性名称的命名约定。

来自 JSX 简介

为什么 constructor 里要调用 super 和传递 props

这是官网的一段代码,具体见:状态(State) 和 生命周期

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }


  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

而且有这么一段话,不仅让我们调用 super 还要把 props 传递进去,但是没有告诉我们为什么要这么做。

不知道你有没有疑惑过为什么要调用 super 和传递 props,接下来我们来解开谜题吧。

为什么要调用 super

其实这不是 React 的限制,这是 JavaScript 的限制,在构造函数里如果要调用 this,那么提前就要调用 super,在 React 里,我们常常会在构造函数里初始化 state,this.state = xxx ,所以需要调用 super。

为什么要传递 props

你可能以为必须给 super 传入 props,否则 React.Component 就没法初始化this.props

class Component {
  constructor(props) {
    this.props = props;
    // ...
  }
}

不过,如果你不小心漏传了 props,直接调用了 super(),你仍然可以在 render和其他方法中访问 this.props(不信的话可以试试嘛)。

为啥这样也行?因为React 会在构造函数被调用之后,会把 props 赋值给刚刚创建的实例对象:

const instance = new YourComponent(props);
instance.props = props;

props 不传也能用,是有原因的。

但这意味着你在使用 React 时,可以用 super() 代替 super(props) 了么?

那还是不行的,不然官网也不会建议你调用 props 了,虽然 React 会在构造函数运行之后,为 this.props 赋值,但在 super() 调用之后与构造函数结束之前,this.props 仍然是没法用的。

// Inside React
class Component {
  constructor(props) {
    this.props = props;
    // ...
  }
}


// Inside your code
class Button extends React.Component {
  constructor(props) {
    super(); //  忘了传入 props
    console.log(props); //  {}
    console.log(this.props); //  undefined
  }
  // ...
}

要是构造函数中调用了某个访问 props 的方法,那这个 bug 就更难定位了。因此我强烈建议始终使用super(props),即使这不是必须的:

class Button extends React.Component {
  constructor(props) {
    super(props); //  We passed props
    console.log(props); //  {}
    console.log(this.props); //  {}
  }
  // ...
}

上面的代码确保 this.props 始终是有值的。

如果你想避免以上的问题,你可以通过class 属性提案 来简化代码:

class Clock extends React.Component {
  state = {date: new Date()};


  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

更详细的内容可见Dan 的博客

为什么组件用大写开头

前面以及说过了,JSX 是 React.createElement(component, props, …children) 提供的语法糖,component 的类型是:string/ReactClass type,我们具体看一下在什么情况下会用到 string 类型,什么情况下用到 ReactClass type 类型

  • string 类型react会觉得他是一个原生dom节点
  • ReactClass type 类型 自定义组件

例如(string):在 jsx 中我们写一个

<div></div>

转换为js的时候就变成了

React.createElement("div", null)

例如(ReactClass type):在jsx中我们写一个

function MyDiv() {
    return (<div><div>)
}

转换为js的时候就变成了

function MyDiv() {
  return React.createElement("div", null);
}


React.createElement(MyDiv, null);

上边的例子中如果将MyDiv中的首字母小写,如下

function myDiv() {
    return (<div><div>)
}

转换为 js 的时候就变成了

function MyDiv() {
  return React.createElement("div", null);
}


React.createElement("myDiv", null);

由于找不到 myDiv 这个 dom,所以就会报错。

为什么调用方法要 bind this

前提知识:深刻的理解 JavaScript 中的 this

相信刚写 React 的时候,很多朋友可能会写类似这样的代码:

class Foo extends React.Component {
  handleClick () {
    this.setState({ xxx: aaa })
  }


  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    )
  }
}

发现会报 thisundefined 的错,然后可能对事件处理比较疑惑,然后去看官网的事件处理有下面一段话:

你必须谨慎对待 JSX 回调函数中的 this,在 JavaScript 中,class 的方法默认不会绑定this。如果你忘记绑定 this.handleClick 并把它传入了 onClick,当你调用这个函数的时候 this 的值为 undefined

这并不是 React 特有的行为;这其实与 JavaScript 函数工作原理有关。通常情况下,如果你没有在方法后面添加 (),例如 onClick={this.handleClick},你应该为这个方法绑定 this

然后你看了官网的例子和建议之后,知道需要为事件处理函数绑定 this就能解决,想下面这样:

class Foo extends React.Component {
  handleClick () {
    this.setState({ xxx: aaa })
  }


  render() {
    return (
      <button onClick={this.handleClick.bind(this)}>
        Click me
      </button>
    )
  }
}

但是可能你没有去思考过为什么需要 bind this?如果你不能理解的话,还是 js 的基础没有打好。

React 是如何处理事件的?

咱们先来了解一下 React 是如何处理事件的。

React 的事件是合成事件, 内部原理非常复杂,我这里只把关键性,可以用来解答这个问题的原理部分进行介绍即可(后面应该会写一篇 react 的事件原理,敬请期待)。

上篇文章已经说过,jsx 实际上是 React.createElement(component, props, …children) 函数提供的语法糖,那么这段 jsx 代码:

 <button onClick={this.handleClick}>
     Click me
 </button>

会被转化为:

React.createElement("button", {
     onClick: this.handleClick
}, "Click me")

了解了上面的,然后简单的理解 react 如何处理事件的,React 在组件加载(mount)和更新(update)时,将事件通过 addEventListener 统一注册到 document 上,然后会有一个事件池存储了所有的事件,当事件触发的时候,通过 dispatchEvent 进行事件分发。

所以你可以简单的理解为,最终 this.handleClick 会作为一个回调函数调用。

理解了这个,然后再来看看回调函数为什么就会丢失 this

this 简单回顾

在函数内部,this的值取决于函数被调用的方式。

如果你不能理解上面那句话,那么你可能需要停下来阅读文章,去查一下相关资料,否则你可能看不懂下面的,如果你懒的话,就看为你准备好的 MDN 吧。

通过上面对事件处理的介绍,来模拟一下在类组件的 render 函数中, 有点类似于做了这样的操作:

class Foo {
    sayThis () {
         console.log(this); // 这里的 `this` 指向谁?
     }


     exec (cb) {
         cb();
     }


    render () {
         this.exec(this.sayThis);
  }
}


var foo = new Foo();
foo.render(); // 输出结果是什么?

你会发现最终结果输出的是 undefined,如果你不理解为什么输出的是 undefined,那么还是上面说的,需要去深刻的理解 this 的原理。如果你能理解输出的是undefined,那么我觉得你就可以理解为什么需要 bind this 了。

那么你可能会问:为什么React没有自动的把 bind 集成到 render 方法中呢?在 exec 调用回调的时候绑定进去,像这样:

class Foo {
    sayThis () {
         console.log(this); // 这里的 `this` 指向谁?
     }


     exec (cb) {
         cb().bind(this);
     }


    render () {
         this.exec(this.sayThis);
  }
}


var foo = new Foo();
foo.render(); // 输出结果是什么?

因为 render 多次调用每次都要 bind 会影响性能,所以官方建议你自己在 constructor 中手动 bind 达到性能优化。

四种事件处理对比

对于事件处理的写法也有好几种,咱们来进行对比一下:

1. 直接 bind this 型

就是像文章开始的那样,直接在事件那里 bind this

class Foo extends React.Component {
  handleClick () {
    this.setState({ xxx: aaa })
  }


  render() {
    return (
      <button onClick={this.handleClick.bind(this)}>
        Click me
      </button>
    )
  }
}

优点:写起来顺手,一口气就能把这个逻辑写完,不用移动光标到其他地方。

缺点:性能不太好,这种方式跟 react 内部帮你 bind 一样的,每次 render 都会进行 bind,而且如果有两个元素的事件处理函数式同一个,也还是要进行 bind,这样会多写点代码,而且进行两次 bind,性能不是太好。(其实这点性能往往不会是性能瓶颈的地方,如果你觉得顺手,这样写完全没问题)

2. constuctor 手动 bind 型

class Foo extends React.Component {
  constuctor(props) {
    super(props)
    this.handleClick = this.handleClick.bind(this)
  }
  handleClick () {
    this.setState({ xxx: aaa })
  }


  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    )
  }
}

优点:相比于第一种性能更好,因为构造函数只执行一次,那么只会 bind 一次,而且如果有多个元素都需要调用这个函数,也不需要重复 bind,基本上解决了第一种的两个缺点。

缺点:没有明显缺点,硬要说的话就是太丑了,然后不顺手(我觉得丑,你觉得不丑就这么写就行了)。

3. 箭头函数型

class Foo extends React.Component {
  handleClick () {
    this.setState({ xxx: aaa })
  }


  render() {
    return (
      <button onClick={(e) => this.handleClick(e)}>
        Click me
      </button>
    )
  }
}

优点:顺手,好看。

缺点:每次 render 都会重复创建函数,性能会差一点。

4. public class fields 型

这种 class fields还处于实验阶段,据我所知目前还没有被纳入标准,具体可见这里。

class Foo extends React.Component {
  handleClick = () => {
    this.setState({ xxx: aaa })
  }


  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    )
  }
}

优点:好看,性能好。

缺点:没有明显缺点,如果硬要说可能就是要多装一个 babel 插件来支持这种语法。

总结

我平时用的就这四种写法,我这边从代码的美观性、性能以及是否顺手方便对各种写法做了简单的对比。其实每种方法在项目里用都是没什么问题的,性能方面基本上可以忽略,对于美观性和顺手比较主观,所以总体来说就是看大家的偏好咯,如果硬要推荐的话,我还是比较推荐第四种写法,美观而且不影响性能。

为什么要 setState,而不是直接 this.state.xx = oo

这个问题是我们公司后端写 React 的时候提出的问题,为啥不能直接修改 state,要 setState 一下。我在想,从 vue 转到 React 可能也会有这种疑问,因为 vue 修改状态都是直接改的。

如果我们了解 setState 的原理的话,可能就能解答这个问题了,setState 做的事情不仅仅只是修改了 this.state 的值,另外最重要的是它会触发 React 的更新机制,会进行 diff ,然后将 patch 部分更新到真实 dom 里。

如果你直接 this.state.xx == oo 的话,state 的值确实会改,但是改了不会触发 UI 的更新,那就不是数据驱动了。

那为什么 Vue 直接修改 data 可以触发 UI 的更新呢?因为 Vue 在创建 UI 的时候会把这些 data 给收集起来,并且在这些 data 的访问器属性 setter 进行了重写,在这个重写的方法里会去触发 UI 的更新。如果你想更多的了解 vue 的原理,可以去购买染陌大佬的剖析 Vue.js 内部运行机制。

不明白访问器属性的可以看这篇文章:深入理解JS里的对象

setState 是同步还是异步相关问题

1. setState 是同步还是异步?

我的回答是执行过程代码同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,所以表现出来有时是同步,有时是“异步”。

2. 何时是同步,何时是异步呢?

只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout/setInterval等原生 API 中都是同步的。简单的可以理解为被 React 控制的函数里面就会表现出“异步”,反之表现为同步。

3. 那为什么会出现异步的情况呢?

为了做性能优化,将 state 的更新延缓到最后批量合并再去渲染对于应用的性能优化是有极大好处的,如果每次的状态改变都去重新渲染真实 dom,那么它将带来巨大的性能消耗。

4. 那如何在表现出异步的函数里可以准确拿到更新后的 state 呢?

通过第二个参数 setState(partialState, callback) 中的 callback 拿到更新后的结果。

或者可以通过给 setState 传递函数来表现出同步的情况:

this.setState((state) => {
    return { val: newVal }
})

5. 那表现出异步的原理是怎么样的呢?

直接讲源码肯定篇幅不够,可以看这篇文章:你真的理解setState吗?。

我这里还是用最简单的语言让你理解:在 React 的 setState 函数实现中,会根据 isBatchingUpdates(默认是 false) 变量判断是否直接更新 this.state 还是放到队列中稍后更新。然后有一个 batchedUpdate 函数,可以修改 isBatchingUpdates 为 true,当 React 调用事件处理函数之前,或者生命周期函数之前就会调用 batchedUpdate 函数,这样的话,setState 就不会同步更新 this.state,而是放到更新队列里面后续更新。

这样你就可以理解为什么原生事件和 setTimeout/setinterval 里面调用 this.state 会同步更新了吧,因为通过这些函数调用的 React 没办法去调用 batchedUpdate 函数将 isBatchingUpdates 设置为 true,那么这个时候 setState 的时候默认就是 false,那么就会同步更新。

最后

setState 是 React 非常重要的一个方法,值得大家好好去研究一下他的原理。

以上就是W3Cschool编程狮关于新手学习 react 迷惑的点(也可以复习,建议收藏!)的相关介绍了,希望对大家有所帮助。

联系我们