复习总结

542 阅读15分钟

一、工程化

  1. import和require有什么区别?

    • ES6 Module 中导入的是值的引用;(当被引用的模块的值被修改以后,引入的值在在被用到的时候也会随之更新,俗称动态加载),而 CommonJS 则是值的拷贝(基础类型是不会变的,如果是引用类型也会随之变化);
    • ES6 模块是编译时输出接口,CommonJS 模块是运行时加载;(因为 CommonJS 加载的是一个对象,即module.exports属性,该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。)

  1. 什么是webpack loader?

    • 处理各种类型文件的一个中间层,将所有文件都转成字符串,并允许对字符串进行任意修改、操作,然后将字符串包在一个对象中返回给webpack;

  1. 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进行打包;

  1. webpack打包流程?

    • 合并webpack.config.js和命令传递的参数,形成最终的配置;
    • 解析配置获取entry入口
    • 读取入口文件内容,通过@babel/parse将入口内容转换成AST树
    • 通过@babel/traverse遍历AST得到各个模块的依赖;
    • 通过@babel/core(实际是@babel/preset-env)将AST转换成es5 code;
    • 通过循环递归的方式拿到所有模块的所有依赖并都转成es5;

  1. import moduleName from 'xxModule'import('xxModule')经过webpack编译打包后最终变成了什么?在浏览器中是怎么运行的?

    • import经过打包之后变成一些Map对象,key为模块路径,value为模块的可执行函数;
    • 代码加载到浏览器之后从入口模块开始执行,webpack会自定义一个__webpack_require__函数,读取Map对象,执行对应的函数,负责实际的加载模块工作;
    • 异步import('xxModule')会单独打成一个包进行动态加载,会动态的在head中创建一个script标签,发送一个http请求,加载对应的模块;

  1. 微前端?(参考:zhuanlan.zhihu.com/p/78362028)

    • 主框架不限制技术栈,子应用具备完全自主权
    • 子应用仓库独立,独立开发、部署
    • 运行时每个子应用状态隔离、互不干涉
    • 针对中后台应用的解决方案:
      • 单实例:同一时刻,只有一个子应用被展示,子应用具备一个完整的生命周期,通常基于url变化来做子应用的切换;
      • 多实例:同一时刻可展示多个子应用。通常使用 Web Components 方案来做子应用封装,子应用更像是一个业务组件而不是应用;

  1. 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服务器上;

  1. 性能优化?

    • 减少http请求,js,css,图片spring
    • 静态资源挂载到cdn上
    • 图片通过服务器进行裁剪,本地图片可以先压缩,图片懒加载懒加载,使用字体图标库
    • 服务器端压缩文件,请求头发送accept-encoding头:gzip
    • 减少重绘重排,避免css复杂性
    • 截流,防抖

二、原生JS

  1. Object.keys、for in、for of的区别
    • Object.keys
      • 返回对象实例自身可枚举属性(键)组成的数组(不返回原型链上的属性以及Symbol属性)
    • for in
      • 遍历对象及其原型链上可枚举的属性
    • for of
      • 支持遍历数组、字符串、Set、Map、类数组对象,不支持遍历普通对象

  1. 手写源码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

  1. 防抖?
/**
 * 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);
  }
}

  1. 截流?
/**
 * 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)
    }
  }
}

  1. 数组去重?
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)
  })
}

  1. 求数组最大、最小值?
function max(arr) {
  return arr.reduce((acc, cur) => Math.max(acc, cur), arr[0]);
}

  1. 数组扁平?
function flatten(arr) {
	return arr.reduce((acc, cur) => {
    return acc.concat(Array.isArray(cur) ? flatten(cur) : cur);
  }, [])
}

  1. 函数柯里化?
    • 使用一个闭包返回一个函数,将外部参数和内部参数结合;
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);

  1. 尾递归?
    • 在函数的尾部返回调用自身,好处是外部函数在最后返回一个内部函数,那么外部函数执行完毕,就会从函数调用栈中被弹出,最后保证函数调用栈中只有一个内部调用函数,而不是大量多个,防止栈溢出。具体解释www.ruanyifeng.com/blog/2015/0…

  1. 数组乱序?
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;
}

  1. 组合函数
const compose = (...fns) => fns.reduce((acc, cur) => (...args) => acc(cur(...args)))

三、面向对象

原型

工厂模式

  1. 定义:抽象创建具体对象的过程,用函数封装创建对象的细节。

    // 创建
    function createPerson(name) {
    	var obj = new Object();
    	obj.name = name;
    	obj.sayName = function() {
    		alert(this.name);
    	}
    	return obj;
    }
    // 调用创建一个新对象
    var person1 = createPerson('Swizerland_K');
    
  2. 优点:

    • 可以解决重复创建多个相似对象的问题
  3. 缺点:

    • 无法识别一个对象的类型(数组、object、function、null等等)

构造函数模式

  1. 定义:因为工厂模式无法识别对象的类型,因此衍生出构造函数模式,可用来创建特定类型的对象,例如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
    
  2. 特点:

    • 没有显式的创建对象;
    • 直接将属性和方法赋值给了this对象;
    • 没有return语句;
  3. new的作用

    • 创建一个新的实例对象;
    • 将构造函数的作用域赋给新对象(因此this指向该实例对象);
    • 执行构造函数中的代码(为实例对象添加属性和方法);
    • 返回一个新的实例对象;
  4. 优点:

    • 创建出来的每个实例都有一个constructor(构造函数)属性,并且都指向Person;
    • 创建构造函数可将它标志为一种特定的对象类型(胜过工厂函数的地方);
  5. 缺点:

    • 每个Person实例都会创建一个Function实例,然后创建多个Function实例没有必要,且会消耗内存;

      function Person(name) {
        this.name = name;
      	this.sayName = new Function('alert(this.name)');
      }
      
  6. 解决缺点:

    • 将函数定义转移到构造函数外部(全局),那么每次构建Person实例对象的时候就会共享一个全局函数sayName,但是这样做就会失去函数封装的意义了。

      function Person(name) {
        this.name = name;
      	this.sayName = sayName;
      }
      function sayName() {
        alert(this.name);
      }
      

原型模式

  1. 定义:每个函数(只有函数具有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
  1. hasOwnProperty() 和In操作符的区别

    • hasOwnProperty()只能检测到对象实例上是否有对应属性,不会去原型对象上去寻找;
    • in操作符,只要被检测的属性在原型链上有,就会返回true;
  2. 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"]
  1. 原型模式的缺点
    • 所有实例会共享原型中的引用类型属性,即如果修改一个实例对象的引用类型属性,也会导致其他实例的该引用类型属性发生修改;

组合使用构造函数模式和原型模式(推荐)

  1. 定义:构造函数用于定义实例属性(不共享的属性),原型模式用于定义方法共享的属性;
// 构造函数
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"]

继承

原型链

  1. 定义:每个对象有一个指向原型对象的链(__proto__),当访问该对象某个属性的时候,会现在该对象上去寻找,如果找不到,会去该对象的原型去搜寻,以及该对象原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾;

  2. 所有引用类型都通过原型链继承了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. 原型对象跟构造函数是没有直接关系的
  1. 使用原型链继承的时候不能使用对象字面量创建原型方法,因为会重写原型链;

  2. 原型链继承问题

  • 引用类型属性会被所有实例共享;
  • 没有办法在不影响所有对象实例的情况下,给父类(被继承的)构造函数传参;

组合继承(推荐)

  1. 定义:通过原型链方式继承原型属性和方法(实现函数复用),通过构造函数继承实例属性(使得每个实例有自己的属性);
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

原型式继承

  1. 定义:简单的继承,无需定义构造函数等,但是引用类型的属性还是会被共享,看情况使用;
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"]

四、作用域

作用域链

  1. 定义:每个函数都有自己的执行环境,在函数执行时,会创建变量对象的作用域链(保障变量和函数有序访问),当前执行代码所在的环境变量对象始终在作用域链的最前端,然后作用域链的下一个变量对象则来自外部环境,依次寻找,最终找到全局执行环境的变量对象,即作用域链的最后一个对象;

静态作用域和动态作用域

  1. 静态作用域:js采用的是静态作用域,函数的作用域在**函数创建(**注意这里不是调用)的时候就定好了;
  2. 动态作用域:函数的作用域是在函数调用的时候才决定的;
var value = 1;

function foo() {
    console.log(value);
}

function bar() {
    var value = 2;
    foo();
}

bar();// 1

五、ES6

  1. 用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]

  1. 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);
});

六、小程序

底层架构

  1. 与当下其他热门的技术 React Native、Weex、Flutter 等不同,小程序的最终渲染载体依然是浏览器内核,而不是原生客户端;
  2. 传统网页js和ui是在同一个线程中进行的,所以会出现阻塞,而微信小程序启用了双线程模式
    • 视图层:webview线程,启用不同的webview来渲染不同的小程序页面;
    • 逻辑层:单独的js线程,控制视图层的逻辑;

![image-20200830153008022](/Users/bo.yang/Library/Application Support/typora-user-images/image-20200830153008022.png)

  1. 任何线程之间的数据传输都是有延时的,因此逻辑层和视图层的通信是异步行为,微信为小程序提供了客户端的原生能力,因此微信主线程和小程序的双线程之间也是异步通信;

启动

  1. 准备运行环境

    • webview基础库(渲染层)
    • Appservice基础库(逻辑层)
  2. 下载小程序代码包

  3. 加载代码包

  4. 初始化首页

优化

  1. 减少代码包中的静态资源文件,图片尽量采用cdn资源加载;
  2. 图片降级处理,可优先加载降质图片,待图片加载完成再显示高清图;
  3. 区分首页模块显示的优先级,优先显示首屏渲染的内容,分块加载,提高首屏渲染速度;
  4. 降低线程间通信频次(减少setData),以及通信数据量,减少wxml节点数量,去掉不必要的事件绑定,去掉不必要的节点属性;
  5. 逻辑向后端移动,一般不涉及前端计算的展示类逻辑,都可以适当做后移
  6. 分包加载;
  7. 合理利用本地缓存;
  8. 页面跳转时预拉取,在页面跳转前先请求数据,然后缓存在全局对象中;
  9. 数据预拉取,可以在小程序启动阶段,通过微信服务器代理小程序客户端发起一个 HTTP 请求到第三方服务器来获取数据, 并且把响应数据存储在本地客户端供小程序前端调取。当小程序加载完成后,只需调用微信提供的 API wx.getBackgroundFetchData 从本地缓存获取数据即可;
  10. 剔除无用文件以及代码,减小代码包大小;

七、React

性能优化

  1. PureComponent、React.memo创建组件,自动浅比较
  2. shouldComponent自定义组件是否需要渲染
  3. 组件懒加载,利用lazy、Suspense
  4. 不要使用内联函数定义和内联样式,因为每次render都会创建一个新函数实例或者对象,导致子组件额外重新加载;
  5. 减少组件安装卸载,频繁变换的情况下尽量用样式控制显示隐藏;

八、终极性能优化篇

  1. juejin.cn/post/684490…