本篇随笔涉及的知识点有
- let和const;
- 继承;
- Proxy;
- 模块化;
- 操作数组map, filter, reduce
- 数据结构set和map
- 数据类型Symbol
1.var,let以及const区别
不被挂载到window:在全局作用域下使用let,const声明变量,变量是不会被挂载到window上的;这一点跟var不同。
无变量提升:let,const声明的变量,不能在声明前使用,存在暂时性死区;var可以进行变量提升。
暂时性死区:虽然变量在编译的环节中被告知在这块作用域中可以访问,但是访问是受限制的。
函数提升:函数提升优先于变量提升,函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪到作用域顶部。
有块状作用域:let
声明的变量只能在当前块级作用域中使用。
禁止重复声明:var
声明的变量可以重复声明,而且不会有任何警告或者提示,覆盖了一个值;而 let
则会直接抛出一个语法错误。
const禁止重复赋值:let
和 const
作用基本一致,但是后者声明的变量不能再次赋值,所以在声明的时候必须赋初值。
2.原型继承和class继承
class继承:js中并不存在类,class是语法糖,本质上还是函数。
class
实现继承的核心在于使用 extends
表明继承自哪个父类,并且在子类构造函数中必须调用 super
,因为这段代码可以看成 Parent.call(this, value)
。
class Parent { constructor(value) { this.val = value } getValue() { console.log(this.val) } } class Child extends Parent { constructor(value) { super(value) } } let child = new Child(1) child.getValue() // 1 child instanceof Parent // true
组合继承:最常见的继承方式,核心是在子类的构造函数中通过Parent.call(this)来继承父类的属性,改变子类的原型为new Parent()来继承父类函数。
function Parent(value) { this.val = value } Parent.prototype.getValue = function() { console.log(this.val) } function Child(value) { Parent.call(this, value) } Child.prototype = new Parent() const child = new Child(1) child.getValue() // 1 child instanceof Parent // true
优点在于构造函数可以传参,不会与父类的属性共享;而可以复用父类的函数。缺点是继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。
寄生组合继承:核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。
function Parent(value) { this.val = value } Parent.prototype.getValue = function() { console.log(this.val) } function Child(value) { Parent.call(this, value) } Child.prototype = Object.create(Parent.prototype, { constructor: { value: Child, enumerable: false, writable: true, configurable: true } }) const child = new Child(1) child.getValue() // 1 child instanceof Parent // true
3.模块化
实现模块化的目的:代码复用;提高代码可维护性;解决命名冲突
模块化的几种方式:
1)立刻执行函数
使用立即执行函数实现模块化是常见的手段,通过函数作用域解决了命名冲突、污染全局作用域的问题。
(function(globalVariable){ globalVariable.test = function() {} // ... 声明各种变量、函数都不会污染全局作用域 })(globalVariable)
2)AMD和CMD:目前这两种实现方式已经很少见到
异步模块定义(AMD)是Asynchronous Module Definition的缩写,是 RequireJS 在推广过程中对模块定义的规范化产出。
它采用异步方式加载模块,模块的加载不影响它后面语句的运行。
所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:
require([module], callback);
第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。
通用模块定义(CMD)是Common Module Definition的缩写,是SeaJS 在推广过程中对模块定义的规范化产出。
CMD 推崇依赖就近,AMD 推崇依赖前置。
// CMD define(function(require, exports, module) { var a = require('./a') a.doSomething() // 此处略去 100 行 var b = require('./b') // 依赖可以就近书写 b.doSomething() // ... }) // AMD 默认推荐的是 define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好 a.doSomething() // 此处略去 100 行 b.doSomething() ... })
3)CommonJS 最早是 Node 在使用,目前也仍然广泛使用,比如在 Webpack 中你就能见到它。
“在 CommonJs 的模块化规范中,每一个文件就是一个模块,拥有自己独立的作用域、变量、以及方法等,对其他的模块都不可见。
CommonJS规范规定,每个模块内部,module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(module.exports)是对外的接口。
加载某个模块,其实是加载该模块的 module.exports 属性。require 方法用于加载模块。”
链接:http://www.imooc.com/article/285854
//moudle-a.js moudle.exports = { a: 1}; //moudle-b.js var ma = require('./moudle-a'); var b = ma.a + 2;module.exports ={ b: b };
4)ES模块
- 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() {}
4.Proxy
概述
Proxy可以用来改变对象的默认操作,比如自行定义set和get等。
Proxy中需要理解的三个属性:
- target: an Object which the proxy virtualizes.(目标对象)
- handler: a Placeholder Object which contains traps.(包含重写方法的对象)
- trap: the Methods that provide property access of the target object.(重写的方法,比如get和set)
比如get方法取不到值时不返回undefined,
let handler = { get: function(target, fieldName) { if(fieldName === 'fullName' ) { return `${target.firstName} ${target.lastName}`; } return fieldName in target ? target[fieldName] : `No such property as, '${fieldName}'!` } }; let p = new Proxy(employee, handler); console.group('proxy'); console.log(p.firstName); console.log(p.lastName); console.log(p.org); console.log(p.fullName); console.groupEnd() proxy Tapas Adhikary No such property as, 'org'! Tapas Adhikary
以及在set方法上加验证及数据绑定等等
const validator = { set: function(obj, prop, value) { if (prop === 'age') { if(!Number.isInteger(value)) { throw new TypeError('Age is always an Integer, Please Correct it!'); } if(value < 0) { throw new TypeError('This is insane, a negative age?'); } } } }; let pr = new Proxy(employee, validator); pr.age = "test"; Uncaught TypeError: Age is always an Integer, Please Correct it! at Object.set (<anonymous>:5:23) at <anonymous>:1:7 set @ VM2381:5 (anonymous) @ VM2434:1 pr.age = -1; Uncaught TypeError: This is insane, a negative age? at Object.set (<anonymous>:8:23) at <anonymous>:1:7 set @ VM2381:8 (anonymous) @ VM2531:1
实例方法
1、get(target, propKey, receiver) 用于拦截某个属性的读取(read)操作,就是在读取目标对象的属性之前,搞点事情。
参数:
target:目标对象
property:属性名
receiver:操作行为所针对的对象,一般指proxy实例本身
let exam={};
let proxy = new Proxy(exam, { get(target, propKey, receiver) { console.log('Getting ' + propKey); return target[propKey]; } })
console.log(proxy.name);//undefined
2、set(target, propKey, value, receiver) 用来拦截目标对象的赋值(write)操作
参数:
target:目标对象
propertyName:属性名
propertyValue:属性值
receiver:Proxy实例本身
let validator = { set: function(obj, prop, value) { if (prop === 'age') { if (!Number.isInteger(value)) { throw new TypeError('The age is not an integer'); return false; } if (value > 200) { throw new RangeError('The age seems invalid'); return false; } } // 对于满足条件的 age 属性以及其他属性,直接保存 obj[prop] = value; return true; } }; let proxy= new Proxy({}, validator) proxy.age = 100; proxy.age // 100 proxy.age = 'oppps' // 报错 proxy.age = 300 // 报错
3、apply(target, ctx, args) 用于拦截函数的调用、call 和 reply 操作。
target 表示目标对象,
ctx 表示目标对象上下文,
args 表示目标对象的参数数组。
function sub(a, b){ return a - b; } let handler = { apply: function(target, ctx, args){ console.log('handle apply'); return Reflect.apply(...arguments); } } let proxy = new Proxy(sub, handler) proxy(2, 1) // 1
4、has(target,propkey) 拦截 propKey in proxy 的操作,返回一个布尔值。即在判断 target 对象是否存在 propKey 属性时,会被这个方法拦截。
let proxy = new Proxy(exam, handler) 'name' in proxy
5、deleteProperty(target, propKey) 拦截delete proxy[propKey]
的操作,返回一个布尔值。用于拦截 delete 操作。
6、construct(target, args) 用于拦截 new 命令。返回值必须为对象。
7、ownKeys(target) 用于拦截对象自身属性的读取操作。
8、getPrototypeOf(target) 拦截对象原型操作
9、isExtensible(target) 用于拦截 Object.isExtensible 操作。
5.操作数组map, filter, reduce
涉及面试题:map, filter, reduce 各自有什么作用?
forEach()
var arr = [1,2,3,4];
arr.forEach((item,index,arr)=>{
console.log(item); //结果为1,2,3,4
});
//forEach遍历数组,无返回值,不改变原数组,仅仅只是遍历,常用于注册组件、指令等等。
map
作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后放入到新的数组中。
[1, 2, 3].map(v => v + 1) // -> [2, 3, 4]
另外 map
的回调函数接受三个参数,分别是当前索引元素,索引,原数组
['1','2','3'].map(parseInt)
- 第一轮遍历
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
这块的内容,同时也是最难理解的一块内容。reduce
可以将数组中的元素通过回调函数最终转换为一个值。
如果我们想实现一个功能将函数里的元素全部相加得到一个值,可能会这样写代码
const arr = [1, 2, 3]
let total = 0
for (let i = 0; i < arr.length; i++) {
total += arr[i]
}
console.log(total) //6
但是如果我们使用 reduce
的话就可以将遍历部分的代码优化为一行代码
const arr = [1, 2, 3]
const sum = arr.reduce((acc, current) => acc + current, 0)
console.log(sum)
对于 reduce
来说,它接受两个参数,分别是回调函数和初始值,接下来我们来分解上述代码中 reduce
的过程
- 首先初始值为
0
,该值会在执行第一次回调函数时作为第一个参数传入 - 回调函数接受四个参数,分别为累计值、当前元素、当前索引、原数组,后三者想必大家都可以明白作用,这里着重分析第一个参数
- 在一次执行回调函数时,当前值和初始值相加得出结果
1
,该结果会在第二次执行回调函数时当做第一个参数传入 - 所以在第二次执行回调函数时,相加的值就分别是
1
和2
,以此类推,循环结束后得到结果 6
some()
var arr = [1,2,3,4];
arr.some((item,index,arr)=>{
return item > 2; //返回true
});
//遍历数组每一项,有一项返回true,则停止遍历,结果返回true。不改变原数组。
every()
var arr = [1,2,3,4];
arr.every((item,index,arr)=>{
return item >1; //返回false
});
//遍历数组每一项,每一项返回true,最终结果为true.有一项返回false,停止遍历,结果返回为false。不改变原数组
6.set和map
ES6提供了新的数据结构Set,类似于数组,但成员的值都是唯一的。它本身是一个构造函数。
const s = new Set(); [2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x)); for (let i of s) { console.log(i); // 2 3 5 4 }
上面代码通过add
方法向Set
结构加入成员,结果表明Set
结构不会添加重复的值。
Set
函数可以接受一个数组(或者具有iterable
接口的其他数据结构)作为参数,用来初始化。
Array.from
方法可以将Set
结构转为数组。
const items = new Set([1, 2, 3, 4, 5]); const array = Array.from(items);
ES6提供了Map,类似于对象,也是键值对的集合,但键的范围不限于字符串,可以是各种类型的值。
也就是说,Object
结构提供了“字符串—值”的对应,Map
结构提供了“值—值”的对应,是一种更完善的Hash
结构实现。如果你需要“键值对”的数据结构,Map
比Object
更合适。
const m = new Map(); const o = {p: 'Hello World'}; m.set(o, 'content') m.get(o) // "content" m.has(o) // true m.delete(o) // true m.has(o) // false
Map
结构的set
方法,将对象o
当作m
的一个键,然后又使用get
方法读取这个键,接着使用delete
方法删除了这个键。上面的例子展示了如何向
Map
添加成员。作为构造函数,Map
也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。const map = new Map([ ['name', '张三'], ['title', 'Author'] ]); map.size // 2 map.has('name') // true map.get('name') // "张三" map.has('title') // true map.get('title') // "Author"
7.Symbol
ES6引入了一种新的原始数据类型Symbol
,表示独一无二的值。它是JavaScript语言的第七种数据类型。Symbol
值通过Symbol
函数生成。
let s = Symbol(); typeof s // "symbol"注意,Symbol
函数前不能使用new
命令,否则会报错。这是因为生成的Symbol
是一个原始类型的值,不是对象。也就是说,由于Symbol
值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。Symbol
函数可以接受一个字符串作为参数,表示对Symbol
实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
let s1 = Symbol('foo'); let s2 = Symbol('bar'); s1 // Symbol(foo) s2 // Symbol(bar) s1.toString() // "Symbol(foo)" s2.toString() // "Symbol(bar)"
还有Promise,箭头函数在其他随笔记录。