一、工程化
-
import和require有什么区别?
- ES6 Module 中导入的是值的引用;(当被引用的模块的值被修改以后,引入的值在在被用到的时候也会随之更新,俗称动态加载),而 CommonJS 则是值的拷贝(基础类型是不会变的,如果是引用类型也会随之变化);
- ES6 模块是编译时输出接口,CommonJS 模块是运行时加载;(因为 CommonJS 加载的是一个对象,即module.exports属性,该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。)
-
什么是webpack loader?
- 处理各种类型文件的一个中间层,将所有文件都转成字符串,并允许对字符串进行任意修改、操作,然后将字符串包在一个对象中返回给webpack;
-
AMD、CMD、CommonJS、ES6的区别?
- AMD:依赖前置(先加载所有依赖的模块,再执行代码)
// 依赖必须一开始就写好 require(['./add', './square'], function(addModule, squareModule) { console.log(addModule.add(1, 1)) console.log(squareModule.square(3)) });- CMD:依赖就近
define(function(require, exports, module) { var addModule = require('./add'); console.log(addModule.add(1, 1)) // 依赖可以就近书写 var squareModule = require('./square'); console.log(squareModule.square(3)) });- CommonJS:require 的时候才去加载模块文件,加载完再接着执行(加载顺序类似CMD)
- CommonJS:同步加载模块(模块文件一般存在于本地硬盘,加载较快)
- AMD:非同步,允许指定回调函数
- ES6:先加载完模块再执行代码
- module.exports、import最后会被babel编译成commonjs语法,但是浏览器并不支持该语法(node支持),所以还需要webpack进行打包;
-
webpack打包流程?
- 合并webpack.config.js和命令传递的参数,形成最终的配置;
- 解析配置获取entry入口
- 读取入口文件内容,通过@babel/parse将入口内容转换成AST树
- 通过@babel/traverse遍历AST得到各个模块的依赖;
- 通过@babel/core(实际是@babel/preset-env)将AST转换成es5 code;
- 通过循环递归的方式拿到所有模块的所有依赖并都转成es5;
-
import moduleName from 'xxModule'和import('xxModule')经过webpack编译打包后最终变成了什么?在浏览器中是怎么运行的?- import经过打包之后变成一些Map对象,key为模块路径,value为模块的可执行函数;
- 代码加载到浏览器之后从入口模块开始执行,webpack会自定义一个
__webpack_require__函数,读取Map对象,执行对应的函数,负责实际的加载模块工作; - 异步import('xxModule')会单独打成一个包进行动态加载,会动态的在head中创建一个script标签,发送一个http请求,加载对应的模块;
-
微前端?(参考:zhuanlan.zhihu.com/p/78362028)
- 主框架不限制技术栈,子应用具备完全自主权
- 子应用仓库独立,独立开发、部署
- 运行时每个子应用状态隔离、互不干涉
- 针对中后台应用的解决方案:
- 单实例:同一时刻,只有一个子应用被展示,子应用具备一个完整的生命周期,通常基于url变化来做子应用的切换;
- 多实例:同一时刻可展示多个子应用。通常使用 Web Components 方案来做子应用封装,子应用更像是一个业务组件而不是应用;
-
webpack优化?
- 开启babel缓存cacheDirectory=true
- tree shaking:去除无用的代码,前提是采用es6模块语法,因为es6模块语法是静态的,可进行静态分析;
- Optimization.splitChunks:抽取公有代码,代码分割(test匹配node_moudules里的文件),设置缓存的chunks;
- externals设置拒绝打包的库
- dll:对第三方库单独进行打包,二次打包的时候就不用再打包了(但是被**
HardSourceWebpackPlugin**替代了) - resolve配置
- alias:准确指定别名路径,减少递归文件去查找
- extensions:后缀名列表,越小越好,将常用文件后缀名放在最前边,减少查询,项目中尽量写明后缀名;
- module.noParse:忽略对没有采用模块化的文件进行递归解析,例如:jquery;
- webpack实现cdn接入:静态资源放在cdn服务器上;
-
性能优化?
- 减少http请求,js,css,图片spring
- 静态资源挂载到cdn上
- 图片通过服务器进行裁剪,本地图片可以先压缩,图片懒加载懒加载,使用字体图标库
- 服务器端压缩文件,请求头发送accept-encoding头:gzip
- 减少重绘重排,避免css复杂性
- 截流,防抖
二、原生JS
- Object.keys、for in、for of的区别
- Object.keys
- 返回对象实例自身可枚举属性(键)组成的数组(不返回原型链上的属性以及Symbol属性)
- for in
- 遍历对象及其原型链上可枚举的属性
- for of
- 支持遍历数组、字符串、Set、Map、类数组对象,不支持遍历普通对象
- Object.keys
- 手写源码call、apply、bind?
// call
Function.prototype.myCall = function(context) {
if (typeof this !== "function") {
throw new Error('error');
}
context = context || window;
// context创建fn属性,并赋值this;
context.fn = this;
// 将call的第一个参数过滤
var args = [...arguments].slice(1);
var result = context.fn(...args);
// 删除“多余”添加的fn
delete context.fn;
return result;
}
// apply
Function.prototype.myApply = function(context) {
if (typeof this !== 'function') {
throw new Error('Error')
}
context = context || window
context.fn = this
let result
// 处理参数和 call 有区别
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}
// bind(不会立即执行,而是返回一个函数)
Function.prototype.myBind = function(context) {
if (typeof this !== "function") {
throw new Error('error');
};
// 保存指向绑定函数的this
const _this = this;
// 将除了context以外的参数提取出来
const args = Array.prototype.slice.call(arguments, 1);
// 中转函数,作为返回函数的原型对象的构造函数,当bind返回的函数被new的时候作为它的构造函数
const ConsFunc = function() {};
const resFunc = function() {
// bind会返回一个函数,此处的arguments是该函数的入参
const bindArgs = Array.prototype.slice.call(arguments);
// return 返回函数的结果,如果返回函数是通过new进行调用的,则this指向他自己的实例,否则this指向context
return _this.apply(this instanceof ConsFunc ? this : context, args.concat(bindArgs));
};
// 修改返回函数的 prototype 为绑定函数的 prototype(拼接原型链),实例就可以继承绑定函数的原型中的值
ConsFunc.prototype = this.prototype;
resFunc.prototype = new ConsFunc();
return resFunc;
}
// 使用bind
var value = 2;
var foo = {
value: 1
};
function bar(name, age) {
this.habit = 'shopping';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = 'kevin';
var bindFoo = bar.myBind(foo, 'daisy');
var obj = new bindFoo('18');
// undefined
// daisy
// 18
console.log(obj.habit); // shopping
console.log(obj.friend); // kevin
- 防抖?
/**
* debounce 防抖函数
* @param {Function} fn 回调函数
* @param {number} delay 延迟毫秒数
* @returns {Function} 回调函数
*/
function debounce(fn, delay) {
var timer = null;
return function() {
var context = this;
var args = arguments;
clearTimeout(timer);
timer = setTimeout(function() {
fn.apply(context, args);
}, delay);
}
}
- 截流?
/**
* throttle 截流函数
* @param {Function} fn 回调函数
* @param {number} delay 延迟毫秒数
* @param {number} mustRunDelay 延迟多少毫秒,强制执行一下
* @returns {Function} 回调函数
*/
function throttle(fn, delay, mustRunDelay) {
var timer = null;
// 设置一个初始化时间
var startTime = null;
return function() {
var context = this;
var args = arguments;
// 当前运行时间
var runCurTime = Date.now();
clearTimeout(timer);
if (!startTime) {
startTime = runCurTime;
}
// 当当前运行时间 - 初始时间大于强制执行时间的时候
if (runCurTime - startTime >= mustRunDelay) {
fn.apply(context, args);
startTime = runCurTime;
} else {
setTimeout(function() {
fn.apply(context, args);
}, delay)
}
}
}
- 数组去重?
function unique(arr) {
var obj = {};
return arr.filter((item) => {
// 把key拼成字符串
var key = typeof item + JSON.stringify(item);
return obj.hasOwnProperty(key) ? false : (obj[key] = true)
})
}
- 求数组最大、最小值?
function max(arr) {
return arr.reduce((acc, cur) => Math.max(acc, cur), arr[0]);
}
- 数组扁平?
function flatten(arr) {
return arr.reduce((acc, cur) => {
return acc.concat(Array.isArray(cur) ? flatten(cur) : cur);
}, [])
}
- 函数柯里化?
- 使用一个闭包返回一个函数,将外部参数和内部参数结合;
function curry(fn) {
var args = Array.prototype.slice.call(arguments, 1);
return function() {
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return fn.apply(null, finalArgs);
}
}
- 特点:参数复用、提前返回、延迟执行;
- 实现一个函数multi(2)(3)(4) 结果等于24?
function curry(fn, args) {
var length = fn.length;
var args = args || [];
return function() {
var newArgs = args.concat(Array.prototype.slice.call(arguments));
if (length > newArgs.length) {
return curry.call(this, fn, newArgs);
} else {
return fn.apply(this, newArgs);
}
}
};
function acc(a,b,c) {
return a * b * c;
};
var multi = curry(acc);
multi(2)(3)(4);
- 尾递归?
- 在函数的尾部返回调用自身,好处是外部函数在最后返回一个内部函数,那么外部函数执行完毕,就会从函数调用栈中被弹出,最后保证函数调用栈中只有一个内部调用函数,而不是大量多个,防止栈溢出。具体解释www.ruanyifeng.com/blog/2015/0…
- 数组乱序?
function shuffle(a) {
for (let i = a.length; i; i--) {
let j = Math.floor(Math.random() * i);
[a[i - 1], a[j]] = [a[j], a[i - 1]];
}
return a;
}
- 组合函数
const compose = (...fns) => fns.reduce((acc, cur) => (...args) => acc(cur(...args)))
三、面向对象
原型
工厂模式
-
定义:抽象创建具体对象的过程,用函数封装创建对象的细节。
// 创建 function createPerson(name) { var obj = new Object(); obj.name = name; obj.sayName = function() { alert(this.name); } return obj; } // 调用创建一个新对象 var person1 = createPerson('Swizerland_K'); -
优点:
- 可以解决重复创建多个相似对象的问题
-
缺点:
- 无法识别一个对象的类型(数组、object、function、null等等)
构造函数模式
-
定义:因为工厂模式无法识别对象的类型,因此衍生出构造函数模式,可用来创建特定类型的对象,例如Object和Array这种原生构造函数;
// 创建构造函数 function Person(name) { this.name = name; this.sayName = function() { alert(this.name); } } // 创建一个新对象 var person1 = new Person('Swizerland_K'); // 我们创建的所有对象既是Object实例,也是Person实例 console.log(person1 instanceof Object); // true console.log(person1 instanceof Person); // true -
特点:
- 没有显式的创建对象;
- 直接将属性和方法赋值给了this对象;
- 没有return语句;
-
new的作用
- 创建一个新的实例对象;
- 将构造函数的作用域赋给新对象(因此this指向该实例对象);
- 执行构造函数中的代码(为实例对象添加属性和方法);
- 返回一个新的实例对象;
-
优点:
- 创建出来的每个实例都有一个constructor(构造函数)属性,并且都指向Person;
- 创建构造函数可将它标志为一种特定的对象类型(胜过工厂函数的地方);
-
缺点:
-
每个Person实例都会创建一个Function实例,然后创建多个Function实例没有必要,且会消耗内存;
function Person(name) { this.name = name; this.sayName = new Function('alert(this.name)'); }
-
-
解决缺点:
-
将函数定义转移到构造函数外部(全局),那么每次构建Person实例对象的时候就会共享一个全局函数sayName,但是这样做就会失去函数封装的意义了。
function Person(name) { this.name = name; this.sayName = sayName; } function sayName() { alert(this.name); }
-
原型模式
- 定义:每个函数(只有函数具有prototype属性)都有一个prototype(原型)属性,它是一个指针,指向一个对象,这个对象包含了所有实例所共享的属性和方法;
function Person() {}
Person.prototype.name = 'bo.yang';
var person1 = new Person();
// 每个对象都有一个内部指针__proto__,并指向构造函数的原型对象
Person.prototype // 构造函数的prototype属性指向原型对象,每个原型对象都有一个constructor属性,constructor是一个指向prototype属性所在函数的指针,此处便是指向Person,即Person.prototype.constructor === Person
-
hasOwnProperty() 和In操作符的区别
- hasOwnProperty()只能检测到对象实例上是否有对应属性,不会去原型对象上去寻找;
- in操作符,只要被检测的属性在原型链上有,就会返回true;
-
Object.keys()和Object.getOwnPropertyNames()
- Object.keys()返回可枚举的实例属性
- Object.getOwnPropertyNames()返回所有实例属性(无论是否可枚举)
function Person() {}
Person.prototype.name = "bo.yang";
Person.prototype.sayName = function() {
alert(this.name);
};
var person1 = new Person();
// 实例上没有赋值属性
Object.keys(person1) // []
Object.getOwnPropertyNames(person1); // []
// 实例上赋值属性
person1.name = 'a biao';
Object.keys(person1) // ["name"]
Object.getOwnPropertyNames(person1); // ["name"]
// 对象原型上的属性
Object.keys(Person.prototype) // ["name", "sayName"]
Object.getOwnPropertyNames(Person.prototype); // ["constructor", "name", "sayName"]
- 原型模式的缺点
- 所有实例会共享原型中的引用类型属性,即如果修改一个实例对象的引用类型属性,也会导致其他实例的该引用类型属性发生修改;
组合使用构造函数模式和原型模式(推荐)
- 定义:构造函数用于定义实例属性(不共享的属性),原型模式用于定义方法和共享的属性;
// 构造函数
function Person(name) {
this.name = 'bo.yang';
this.friends = ['shelly'];
}
// 原型模式
Person.prototype = {
constructor: Person,
sayName: function() {
alert(this.name);
}
};
var person1 = new Person('minghui');
var person2 = new Person('a biao');
person1.friends.push('liuchao');
console.log(person1.friends); // ["shelly", "liuchao"]
console.log(person2.friends); // ["shelly"]
继承
原型链
-
定义:每个对象有一个指向原型对象的链(
__proto__),当访问该对象某个属性的时候,会现在该对象上去寻找,如果找不到,会去该对象的原型去搜寻,以及该对象原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾; -
所有引用类型都通过原型链继承了Object,所有函数默认的原型都是Object实例;
原型链继承
function Father() {
this.name = "father";
}
Father.prototype.getFatherName = function() {
return this.name;
}
function Child() {
this.childName = "child";
}
// Father的实例赋值给Child原型,完成Child继承Father属性和方法的操作
Child.prototype = new Father();
Child.prototype.getChildName = function() {
return this.childName;
}
var children = new Child();
console.log(children.getFatherName()); // "father"
// 原型链
children.__proto__ === Child.prototype // true
// Child.prototype可以看做是Father.prototype的实例
Child.prototype.__proto__ === Father.prototype // true
Father.prototype.__proto__ === Object.prototype // true
Object.prototype.__proto__ // null
// 总结:
// 1. 实例有内部指针__proto__
// 2. 原型对象也有内部指针__proto__
// 3. 原型对象跟构造函数是没有直接关系的
-
使用原型链继承的时候不能使用对象字面量创建原型方法,因为会重写原型链;
-
原型链继承问题
- 引用类型属性会被所有实例共享;
- 没有办法在不影响所有对象实例的情况下,给父类(被继承的)构造函数传参;
组合继承(推荐)
- 定义:通过原型链方式继承原型属性和方法(实现函数复用),通过构造函数继承实例属性(使得每个实例有自己的属性);
function Father(name) {
this.name = name;
this.colors = ['red'];
}
Father.prototype.sayName = function() {
console.log(this.name);
}
function Child(name, age) {
// 做到每个实例都有自己的属性
Father.call(this, name);
this.age = age;
}
// Father的实例赋值给Child原型,完成Child继承Father属性和方法的操作
Child.prototype = new Father();
// 防止因为重写原型而导致constructor丢失
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() {
console.log(this.age);
}
var children1 = new Child('bo.yang', 28);
children1.colors.push('blue');
console.log(children1.colors); // ["red", "blue"]
children1.sayName(); // "bo.yang"
children1.sayAge(); // 28
var children2 = new Child('a biao', 27);
console.log(children2.colors); // ["red"]
children1.sayName(); // "a biao"
children1.sayAge(); // 27
原型式继承
- 定义:简单的继承,无需定义构造函数等,但是引用类型的属性还是会被共享,看情况使用;
var person = {
name: 'bo.yang',
friends: ['ming hui', 'a biao']
}
var person1 = Object.create(person);
person1.name = 'liu chao';
person1.friends.push('en yue');
console.log(person.name, person.friends); // bo.yang ["ming hui", "a biao", "en yue"]
console.log(person1.name, person.friends);// liu chao ["ming hui", "a biao", "en yue"]
四、作用域
作用域链
- 定义:每个函数都有自己的执行环境,在函数执行时,会创建变量对象的作用域链(保障变量和函数有序访问),当前执行代码所在的环境变量对象始终在作用域链的最前端,然后作用域链的下一个变量对象则来自外部环境,依次寻找,最终找到全局执行环境的变量对象,即作用域链的最后一个对象;
静态作用域和动态作用域
- 静态作用域:js采用的是静态作用域,函数的作用域在**函数创建(**注意这里不是调用)的时候就定好了;
- 动态作用域:函数的作用域是在函数调用的时候才决定的;
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();// 1
五、ES6
- 用reduce实现filter?
Array.prototype.myFilter = function(callback) {
if (typeof callback === 'function') {
return this.reduce((acc, cur, index, arr) => {
callback(cur, index, arr) ? acc.push(cur) : null;
return acc;
}, []);
} else {
console.log(new Error('callback is not function'))
}
}
[1, 5, 6]._filter(item => item > 2) // [5, 6]
- promisify实现
function promisify(original) {
return function(...args) {
return new Promise((resolve, reject) => {
args.push(function callback(err, ...values) {
if (err) {
return reject(err)
} else {
return resolve(...values);
}
});
original.call(this, ...args);
})
}
}
// 使用
var fs = require("fs");
let promisifyReadDir = promisify(fs.readdir);
promisifyReadDir("./").then((files) => {
console.log(files);
});
六、小程序
底层架构
- 与当下其他热门的技术 React Native、Weex、Flutter 等不同,小程序的最终渲染载体依然是浏览器内核,而不是原生客户端;
- 传统网页js和ui是在同一个线程中进行的,所以会出现阻塞,而微信小程序启用了双线程模式
- 视图层:webview线程,启用不同的webview来渲染不同的小程序页面;
- 逻辑层:单独的js线程,控制视图层的逻辑;

- 任何线程之间的数据传输都是有延时的,因此逻辑层和视图层的通信是异步行为,微信为小程序提供了客户端的原生能力,因此微信主线程和小程序的双线程之间也是异步通信;
启动
-
准备运行环境
- webview基础库(渲染层)
- Appservice基础库(逻辑层)
-
下载小程序代码包
-
加载代码包
-
初始化首页
优化
- 减少代码包中的静态资源文件,图片尽量采用cdn资源加载;
- 图片降级处理,可优先加载降质图片,待图片加载完成再显示高清图;
- 区分首页模块显示的优先级,优先显示首屏渲染的内容,分块加载,提高首屏渲染速度;
- 降低线程间通信频次(减少setData),以及通信数据量,减少wxml节点数量,去掉不必要的事件绑定,去掉不必要的节点属性;
- 逻辑向后端移动,一般不涉及前端计算的展示类逻辑,都可以适当做后移;
- 分包加载;
- 合理利用本地缓存;
- 页面跳转时预拉取,在页面跳转前先请求数据,然后缓存在全局对象中;
- 数据预拉取,可以在小程序启动阶段,通过微信服务器代理小程序客户端发起一个 HTTP 请求到第三方服务器来获取数据, 并且把响应数据存储在本地客户端供小程序前端调取。当小程序加载完成后,只需调用微信提供的 API
wx.getBackgroundFetchData从本地缓存获取数据即可; - 剔除无用文件以及代码,减小代码包大小;
七、React
性能优化
- PureComponent、React.memo创建组件,自动浅比较
- shouldComponent自定义组件是否需要渲染
- 组件懒加载,利用lazy、Suspense
- 不要使用内联函数定义和内联样式,因为每次render都会创建一个新函数实例或者对象,导致子组件额外重新加载;
- 减少组件安装卸载,频繁变换的情况下尽量用样式控制显示隐藏;