企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
### var、let和const区别 **什么是提升?什么是暂时性死区?var、let 及 const 区别?** ~~~ console.log(a) // undefined var a = 1 相当于 var a ; console.log(a) // undefined a = 1 ~~~ 1. 变量提升:虽然变量还没有被声明,但是我们却可以使用这个未被声明的变量,这种情况叫做提升,**并且提升的是声明**。 ~~~ var a = 10 var a console.log(a) //10 相当于 var a; var a; a = 10 ~~~ 2. 不仅变量会提升,函数也会提升,而且函数提升优先于变量提升。 ~~~ console.log(a); //f a(){} function a(){}; var a = 1; console.log(a); //undefined; var a = function(){}; var a = 1; ~~~ 3. let和const * **全局作用域下,使用let和const声明变量,变量并不会挂载到window上,这和var声明有了区别。** * **当我们在声明a之前使用了a,就会出现报错**。 * 报错原因:存在**暂时性死区**,我们**不能在声明前就使用变量**,这也是let和const优于var的一点。(这里你认为的提升和var的提升是有区别的,虽然变量在编译的环节中被告知在这块作用域中可以访问,但是访问时受限制的)。 ~~~ var a = 1; let b = 1; const c = 1; console.log(window.a);//1 console.log(window.b);//undefined console.log(window.c);//undefined function test(){ console.log(a); //1 let a; } test(); //报错 ~~~ 4. 为什么要存在变量提升?**根本原因是为了解决函数间相互调用的情况**。 #### **总结** 1. 函数提升优先于变量提升,函数提升会把**整个函数挪到作用域顶部**,变量提升只会把**声明挪到作用域顶部** 2. var存在提升,我们能在声明之前使用。let const因为暂时性死区的原因,不能在声明前使用。 3. var 在全局作用域下声明变量会导致变量挂载在window下,其他两者不会 4. let和const作用基本一致,但是后者声明的变量不能再次赋值。 ### 原型链继承和Class继承 原型链如何实现继承?Class如何实现继承?Class本质是什么? 1. 在JS中并不存在类,class只是语法糖,本质还是函数 ~~~ class Person{} Person instanceof Function //true ~~~ 2. 分别饰演原型和class的方法来实现继承 #### 组合继承 组合继承是最常用的继承方式 ~~~ 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 instanceof Parent //true; ~~~ 核心:**子类的构造函数中,通过 `Parent.call(this)`继承父类的属性,然后改变子类的原型为` new Parent()`来继承父类的函数** 优点:构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数 缺点:继承父类函数的时候,调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存的浪费 ![](https://box.kancloud.cn/253cdebb847710f919f804cd6a92ecbd_322x152.png) #### 几声组合继承 对组合继承进行了优化,**组合继承缺点在于继承父类函数时调用了构造函数**,我们只需要优化掉这点就行。 ~~~ 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 ~~~ 核心:将父类的原型赋值给了子类,并且将构造函数设置为子类。 优点:既解决了无用的父类属性问题,还能正确的找到子类的构造函数。 ![](https://box.kancloud.cn/120215cc72b4919abb1605ee4aa3c068_272x140.png) #### Class继承 以上两种继承方式都是通过原型去解决的,在ES6中,我们可以使用class去实现继承,并且实现起来简单 ~~~ class Parent { constructor(value){ this.val = value; } getValue(){ console.log(this.val) } } class Child extends Parent{ constructor(value){ super(value) this.val = value; } } let child = new Child(1); child.getValue();//1 child instanceof Parent // true ~~~ class实现继承的核心:**使用`extends`表明继承自哪个父类,并且在子类构造函数中必须调用`super`,因为这段代码可以看成`Parent.call(this,value)`** js中并不存在类,**class的本质是函数**。 ### 模块化 为什么使用模块化?都有哪几种方式可以实现模块化,各有什么特点? 1. 解决命名冲突 2. 提供复用性 3. 提供代码可维护性 4. (文件依赖解决) #### 立即执行函数 早期,立即执行函数实现模块化是常见的手段,通过函数作用域解决了**命名冲突,污染全局作用域**的问题 ~~~ (function( globalVariable ){ globalVariable.test = function(){} //声明各种变量,函数都不会污染全局作用域 })(globalVariable) ~~~ #### AMD(RequireJS)和CMD(SeaJS) ~~~ define(['./a', './b'], function(a, b){ //加载模块完毕,可以使用 a.do(); b.do(); }) //CMD define(function(require,exports,module){ //加载模块 //可以把require写在函数体的任意地方实现延迟加载 var a = require('./a'); a.doSomething(); }) ~~~ #### CommonJS CommonJS最早是Node在使用,目前广泛应用,比如在webpack中 ~~~ //a.js module.exports = {a:1} //or exports.a = 1; //b.js var module = require('./a.js'); module.a //1 ~~~ 因为CommonJS还是会使用的,所有说一下疑难点 * require ~~~ var module = require('./a.js'); module.a //这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了 //重要的是module这里,module是Node独有的一个变量 module.exports = { a: 1}; //module 基本实现 var module = { id: 'xx',//我总得知道怎么去找他吧 exports: {} // exports 就是个空对象 } //这个是为什么exports和module.exports用法相似的原因 var exports = module.exports; var load = function(module){ //导出的东西 var a =1; module.exports = a; return module.exports; }; // 然后当我require的时候去找到独特的id,然后将要使用的东西用立即执行函数包装下。 ~~~ 另外,虽然exports和module.exports用法相似,但是不能对exports直接赋值。因为 `var exports = module.exports `,这句表明了exports和module.exports享有相同地址。如果直接对exports赋值就会导致两者不再指向同一个内存地址。 #### ES Module(ES6的import和export) 这个是ECMA的一套,是官方的 ~~~ // 报错1 export 1; // 报错2 const m = 1; export m; // 接口名与模块内部变量之间,建立了一一对应的关系 // 写法1 export const m = 1; // 写法2 const m = 1; export { m }; // 写法3 const m = 1; export { m as module }; ~~~ 不能直接导出变量,但是可以导出声明(函数,变量声明),export之后只能接声明或者语句 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() {} ~~~ ### Proxy **Proxy(构造器)可以实现什么功能?** Vue3.0中将会通过`Proxy`来替换原来的`Object.defineProperty`来实现数据响应式。Proxy是ES6中新增的功能,它可以用来定义对象中的操作。(使用它,你可以在对象与各种操作对象的行为直接收集有关请求操作的各种信息,并返回任何你想做的,代理Proxy与中间件有很多共同点)(Proxy代理能够让你截取原对象的各种操作,get set apply和construct),看这个规定有很多被拦截的方法。 ~~~ let p = new Proxy(target,handle) ~~~ target:代表需要添加代理的对象,handler用来自定义对象中的操作(如set或者get) ~~~ 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 ~~~ 通过自定义set和get函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写是发出通知。 如果需要实现一个Vue中的响应式,需要我们**在get中收集依赖**,**在set派发更新**。 在Vue3.0中使用Proxy替换原本的API,原因: **Proxy无需一层层递归为每个属性添加代理,一次性即可完成以上操作,性能上更好,并且原本的实现由一些数据更新不能监听到,但Proxy可以完美监听到任何方式的数据改变,唯一缺陷可能就是浏览器的兼容性不好** #### Reflect.get #### map,filter,reduce map,filter,reduce各自有什么作用 1. map作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后放入新的数组中 ~~~ [1,2,3].map(v =>v+1) // ->[2,3,4] ~~~ 另外map的回调函数接收三个参数(**当前所有元素,索引,原数组**) ~~~ ['1', '2', '3'].map(parseInt) //[1, NaN, NaN]; ~~~ ~~~ ['1', '2', '3'].map(function(){ console.log(arguments); //['1', 0, Array[3]] ... }) ~~~ parseInt(string, radix); parseInt()函数将给定的字符串以指定基数(radix/base)解析成为正式radix 传入0时会把1当成是10进制数,所以“1”成功了。 radix传入1时...没有1进制数,所以不可能转换成功,返回NaN radix传入2时,"3"不能当作2进制数处理所以也返回NaN ~~~ console.log(parseInt('010',10)); // 输出10 console.log(parseInt('010',8)); // 输出8 console.log(parseInt('0x10',10)); // 输出0 console.log(parseInt('0x10',16)); // 输出16 ~~~ * 第一轮遍历`parseInt('1', 0) -> 1` * 第二轮遍历`parseInt('2', 1) -> NaN` * 第三轮遍历`parseInt('3', 2) -> NaN` 2. filter的作用也是生成一个新数组,在遍历数组的时候将返回值为true的元素放入新数组(可以删除一些不需要的元素) ~~~ let array = [1, 2, 4, 6]; let newArray = array.filter(item => item !=6) // [1,2,4] ~~~ filter的回调和map一样接收三个参数 3. reduce,可以将**数组中的元素通过回调函数最终转换为一个值**。 ~~~ const arr = [1, 2, 3]; const sum = arr.reduce((acc,current) =>{ return acc+current },0) console.log(sum) ~~~ reduce接受两个参数,(**回调函数和初始值**) 分析: 1. 首先初始值为0,执行第一次回调函数是作为第一个参数传入 2. 回调函数接受四个参数(**累计值,当前元素,当前索引,原数组**) 3. 第一次回调,当前值(1),初始值(0),得到1,1作为第二次执行回调函数的第一个参数 4. 第二次执行回调函数时,当前值(2),初始值(1)得到3 ~~~ const arr = [1,2,3]; const mapArray = arr.map( value => value*2); const reduceArray = arr.reduce( (acc,current)=>{ acc.push(current*2); return acc; //一定要有return!! } ,[]) console.log(mapArray); console.log(reduceArray); console.log(mapArray,reduceArray); ~~~