阅读 1052

ES6少许知识点|小册免费学

var、let、const

变量提升

变量虽然在用之后再声明,但是能用,这是因为变量会提升

console.log(a) // undefined
var a = 1
=》等同于
var a
console.log(a) // undefined
a = 1
复制代码
var a = 10
var a
console.log(a)
=》
var a
var a
a = 10
console.log(a)
复制代码

函数也会:var声明的会提升到顶部,函数会把整个挪到上面

console.log(a) // ƒ a() {}
function a() {}
var a = 1
=》
var a
function a() {}
console.log(a)
a=1
复制代码
  • var声明的变量会挂载在window里,let/const不会
  • let/const会产生暂时性死区,在声明前使用会报referenceError
  • let 和 const 作用基本一致,但是后者声明的变量不能再次赋值
  • let和const不允许重复声明

为什么要有变量提升?

为了解决函数间互相调用的情况

function test1() {
    test2()
}
function test2() {
    test1()
}
test1()
复制代码

因为不可能存在 test1 在 test2 前面然后 test2 又在 test1 前面。

我自己总结的let和const实现

babel在let定义的变量前加了道下划线,避免在块级作用域外访问到该变量,也可用自执行函数来模拟块级作用域

利用Object.defineProperty()来代理这个值,令其writable为false,使其不可被修改,不过代理对象属性值是可以被修改的哈

继承

原型链继承

缺点:引用属性被改所有实例受影响;创建实例不能传参

function Person(){}
functoin Child(){}
Child.prototype = new Person()
Child.prototype.constructor = Child
复制代码

构造函数继承

构造函数内重复创建⽅法,导致内存占⽤过多。

function Child(){Parent.apply(this,argument)}
function Child(id){
Parent.apply(this,Array.form(argument).slice(1);this.id = id;)}
new Child(id,'xx')
复制代码

组合继承

上面两种的组合 调用了两次构造函数,有重复操作

在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费

Parent.prototype.eat = function () {
 console.log(`${this.name} - eat`);
};
function Child(){Parent.apply(this,argument)}
Child.prototype = new Parent()
Child.prototype.constructor = Child
const child = new Child(1)

child.eat() // 1
child instanceof Parent // true
复制代码

image.png

寄生组合继承

组合继承缺点在于继承父类函数时调用了构造函数,我们只需要优化掉这点就行了。

以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数

function Child() {
  Parent.apply(this, arguments);
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child
复制代码

image.png

class继承

class Parent {
  constructor(value) {
    this.val = value
  }
  getValue() {
    console.log(this.val)
  }
}
class Child extends Parent {
  constructor(value) {
    super(value)
  }
}
复制代码

class 实现继承的核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super,因为这段代码可以看成 Parent.call(this, value)。

在 JS 中并不存在类,class 只是语法糖,本质还是函数。

class Person {}
Person instanceof Function // true
复制代码

模块化

juejin.cn/post/684490…

好处:

  • 解决命名冲突
  • 提供复用性
  • 提高代码可维护性

早期

在早期,用的是立即执行函数来避免命名冲突和污染全局变量

(function(globalVariable){
   globalVariable.test = function() {}
   // ... 声明各种变量、函数都不会污染全局作用域
})(globalVariable)
复制代码

AMD和CMD

通过require(['moduleA', 'moduleB'], function(moduleA, moduleB) {}使用, define定义

需要引入支持规范的脚本文件require.js

不能直接运行在node端

// AMD
define(['./a', './b'], function(a, b) {
  // 加载模块完毕可以使用
  a.do()
  b.do()
})
// CMD
define(function(require, exports, module) {
  // 加载模块
  // 可以把 require 写在函数体的任意地方实现延迟加载
  var a = require('./a')
  a.doSomething()
})
//UMD
同构,同时运⾏在浏览器端和 Node.js ⾥⾯
检测module和define是否存在确定是什么环境,如何用对应的方式去定义模块
复制代码

CJS

  • 使用
// a.js
module.exports = {
    a: 1
}
// or 
exports.a = 1

// b.js
var module = require('./a.js')
module.a // -> log 1
复制代码

require简单原理:

var module = require('./a.js')
module.a 
// 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,
// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {
    a: 1
}
// module 基本实现
var module = {
  id: 'xxxx', // 我总得知道怎么去找到他吧
  exports: {} // exports 就是个空对象
}
// 这个是为什么 exports 和 module.exports 用法相似的原因
var exports = module.exports 
var require = function (module) {
    // 导出的东西
    var a = 1
    module.exports = a
    return module.exports
};
// 然后当我 require 的时候去找到独特的id,
// 然后将要使用的东西用立即执行函数包装下,返回导出的对象

//对于目前的我来说,更认为是这样的
map = {
    ./a.js:'1'
}
list = {
    1:'a=1'
}
var require = fucntion(module){
    //module 为 ./a.js
    //return list[map[module]]
    return module.exports
}
复制代码

exports module.exports 享有相同地址,通过改变对象的属性值会对两者都起效,但是如果直接对 exports 赋值就会导致两者不再指向同一个内存地址,修改并不会对module.exports起效

ES module

ES Module 是原生实现的模块化方案,与 CommonJS 有以下几个区别

  • CommonJS 支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
  • CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
  • CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
  • ES Module 会编译成 require/exports 来执行的
// 引入模块 API
import XXX from './a.js'
import { XXX } from './a.js'
// 导出模块 API
export function a() {}
export default function() {}
复制代码

更全的区别: mp.weixin.qq.com/s/TbwDzQcW8…

1.加载方式不同
使用 `require()``module.exports``exports`的属性赋值;
ESM 使用 `import``export (defalut)`,可以按需加载;
2.运行机制不同
es6中,js引擎对脚本静态分析时,把模块加载换成只读引用,read-only特性,不能被赋值;等待脚本真正执行的时候去异步加载模块,通过引用去模块中获取值,由于引用导入,模块内部值变化会影响外部值的导入
cjs是运行时同步加载,require会从硬盘中读文件,然后立刻执行代码,输出的是⼀个值的拷⻉,cjs会缓存模块结果
3.运行环境不同
cjs主要运行在服务端,cjs是node模块化规范
esm主要运行在浏览器端,js-core层面的
4.两文件互相依赖时
esm是中断执行,报引用错误;cjs暂停执行,无错误;

cjs this指向当前模块,es6指向undefind
CJS 使用 `require()``module.exports``exports`的属性赋值;ESM 使用 `import``export`
CommonJS 模块输出的是⼀个值的拷⻉,ES6 模块输出的是值的引⽤
CJS是运行时加载的,require会从硬盘中进入读操作,然后立刻执行代码,module.exports在脚本运行时生成。
ESM模块加载是异步的,编译时输出接口,也就是说js引擎对脚本静态分析时获取到的是一个只读的引用,执行时会异步下载代码,并行下载代码,但是是按顺序执行的。
ESM是js core层面的,其他是运行环境上的规范
复制代码

proxy

get读取,set设置,has属性in对象,delete删除对象, ownKeys拦截Object.getOwnPropertyNames(proxy),Object.keys(proxy)、for...in循环 还有其他拦截Object原型上的方法,拦截 Proxy 实例作为函数调用和构造函数调用的操作,调用call,apply和new

优点: 性能和效果更好,兼容性差

作用:

自定义对象的操作,Vue3.0 使用来进行数据响应

vue3怎么设的

let onWatch = (obj, setBind, getLogger) => {
  let handler = {
    get(target, property, receiver) {
      getLogger(target, property)
      return Reflect.get(target, property, receiver)
    },
    set(target, property, value, receiver) {
      setBind(value, property)
      return Reflect.set(target, property, value)
    }
  }
  return new Proxy(obj, handler)
}

let obj = { a: 1 }
let p = onWatch(
  obj,
  (v, property) => {
    console.log(`监听到属性${property}改变为${v}`)
  },
  (target, property) => {
    console.log(`'${property}' = ${target[property]}`)
  }
)
p.a = 2 // 监听到属性a改变
p.a // 'a' = 2

get(target, property, receiver) {
    getLogger(target, property)
    // 这句判断代码是新增的
    if (typeof target[property] === 'object' && target[property] !== null) {
        return new Proxy(target[property], handler);
    } else {
        return Reflect.get(target, property);
    }
}
复制代码

map, filter, reduce

map 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后放入到新的数组中。

[1, 2, 3].map(v => v + 1) // -> [2, 3, 4] 另外 map 的回调函数接受三个参数,分别是当前索引元素,索引,原数组

['1','2','3'].map(parseInt)
parseInt(string, radix) 解析的字符,
radix:几进制,介于 2 ~ 36 之间。
如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以 “0x” 或 “0X” 开头,将以 16 为基数。 parseInt('11', 2)=>3

如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。
第一轮遍历 parseInt('1', 0) -> 1
第二轮遍历 parseInt('2', 1) -> NaN
第三轮遍历 parseInt('3', 2) -> NaN
复制代码

filter 的作用也是生成一个新数组,在遍历数组的时候将返回值为 true 的元素放入新数组,我们可以利用这个函数删除一些不需要的元素

let array = [1, 2, 4, 6]
let newArray = array.filter(item => item !== 6)
console.log(newArray) // [1, 2, 4]
复制代码

和 map 一样,filter 的回调函数也接受三个参数,用处也相同。

  • reduce

juejin.cn/post/684490…

如果我们想实现一个功能将函数里的元素全部相加得到一个值


const arr = [1, 2, 3]
const sum = arr.reduce((acc, current) => acc + current, 0)
console.log(sum)
复制代码

对于 reduce 来说,它接受两个参数,分别是回调函数和初始值,接下来我们来分解上述代码中 reduce 的过程

首先初始值为 0,该值会在执行第一次回调函数时作为第一个参数传入

回调函数接受四个参数,分别为累计值、当前元素、当前索引、原数组,后三者想必大家都可以明白作用,这里着重分析第一个参数

在一次执行回调函数时,当前值和初始值相加得出结果 1,该结果会在第二次执行回调函数时当做第一个参数传入

所以在第二次执行回调函数时,相加的值就分别是 1 和 2,以此类推,循环结束后得到结果 6

想必通过以上的解析大家应该明白 reduce 是如何通过回调函数将所有元素最终转换为一个值的,当然 reduce 还可以实现很多功能,接下来我们就通过 reduce 来实现 map 函数

const arr = [1, 2, 3]
const mapArray = arr.map(value => value * 2)
const reduceArray = arr.reduce((acc, current) => {
  acc.push(current * 2)
  return acc
}, [])
console.log(mapArray, reduceArray) // [2, 4, 6]
复制代码
  • 本文正在参与「掘金小册免费学啦!」活动, 点击查看活动详情
文章分类
前端
文章标签