1. 前端模块化
模块化就是将一个复杂系统分解为多个独立模块的代码组织形式。
模块化的好处:
- 避免命名冲突
- 更好的分离,实现按需加载
- 更高的复用性
- 高可维护性
模块化的发展:
- IIFE 立即执行函数
- 模块化的一大作用就是隔离作用域,避免变量冲突
- 无法解决依赖管理的问题,只能手动维护script的相对顺序
- AMD 主要提供异步加载功能,可以指定回调函数(require.js)
- 提前预加载 define/require
- 浏览器端一般使用该规范
// a.js
define(function() {
return {
test: function() {
console.log('test');
}
};
});
// b.js
require(['a', 'other'], function(a, other) {
a.test(); // test
});
- CMD 加载完模块之后并不执行,遇到require之后才执行相应模块
- CMD推崇就近依赖,只有在用到某个模块时再去require
- CMD异步加载
- CMD模块默认是延迟执行,AMD是提前执行的
- CommonJS 同步阻塞式加载,无法实现按需异步加载(Nodejs)
- 用于服务器端
- 运行时加载,需要使用browserify提前编译打包
- 所有代码运行在模块作用域,不会污染全局作用域
- 模块可以多次加载,但是只会在第一次加载时运行一次,结果会被缓存
- 模块加载的顺序是其在代码中出现的顺序
- 通过modules.exports导出模块,require导入模块
- 每个文件是一个模块,有自己的作用域,对其他文件不可见。如果想在多个文件中共享变量,可以定义为global的属性。
- Node中提供一个exports变量指向module.exports
// a.js
var a = 'test';
function foo() {
console.log('foo');
}
module.exports = {
a,
foo
};
// b.js
let {a, foo} = require('./a.js');
console.log(a);
foo();
// test foo
- ES6 Modules 提供了import/export命令
- 浏览器和服务器端的通用方案
- 基于文件的模块,必须一个文件一个模块
- ES6模块的API是静态的,不能在运行过程中添加方法
- 单例模式,每次对同一个模块的导入都指向同一个实例
- 尽量的静态化,使得编译时就能确定模块的依赖关系。
- CommonJS和AMD模块,只能在运行时确定依赖
- ES6 import导入的模块都是原模块的引用
// a.js
var a = 'test';
function foo() {
console.log('foo');
}
export {a, foo}
// b.js
import {a, foo} from './a.js';
foo(); // foo
// 还可以使用export default导出,但一个文件中只能有一个default
var a = 'test';
export default a
// b.js
// 本质上输出名为default的变量,导入时可以为它取任意名字
import a from './a.js';
console.log(a)
import与require的区别:
- require是AMD规范引入方式,import是ES6的标准语法
- require是运行时调用,可以放在代码的任何地方,而import是编译时调用,必须放在文件开头
- require时会去执行整个模块的代码,而import不会执行,只生成一个动态的只读引用
- require是赋值过程,将结果赋值给某个变量;而import是解构过程,导出的对象与整个模块进行解构赋值。
- require模块加载会缓存,而ES6中没有缓存的问题,实时加载
模块的循环加载问题:
CommonJS的循环加载
require第一次加载脚本时就会执行整个脚本,在内存中生成该模块的说明对象。
{
id: '', //模块名,唯一
exports: { //模块输出的各个接口
...
},
loaded: true, //模块的脚本是否执行完毕
...
}
一旦出现循环加载,就只输出已经执行的部分,没有执行的部分不输出。
// main.js
let a = require('./a.js');
let b = require('./b.js'); // 不会在执行,从缓存中取
console.log('main.js', '执行完毕', a.done, b.done);
// a.js
exports.done = false;
let b = require('./b.js');
console.log('a.js', b.done);
exports.done = true;
console.log('a.js', '执行完毕');
//b.js
exports.done = false;
let a = require('./a.js'); // 此时回去缓存中找,而a.js未执行完,此时a.done为false
console.log('b.js', a.done);
exports.done = true;
console.log('b.js', '执行完毕');
// 执行main.js
// 输出
// b.js false
// b.js 执行完毕
// a.js true
// a.js 执行完毕
// main.js 执行完毕 true true
ES6的循环加载
ES6模块是动态引用,遇到模块加载命令import时不会去执行模块,只是生成一个指向被加载模块的引用。
// even.js
import {odd} from './odd';
var counter = 0;
export function even(n){
counter ++;
console.log(counter);
return n == 0 || odd(n-1);
}
// odd.js
import {even} from './even.js';
export function odd(n){
return n != 0 && even(n-1);
}
// main.js
import * as m from './even.js';
var x = m.even(5);
console.log(x);
var y = m.even(4);
console.log(y);
// 1 2 3 false
// 4 5 6 true
2. 前端性能优化
主要目的就是让页面的打开速度更快,以得到更好的用户体验。
- 减少HTTP请求数量
- 合理设置HTTP缓存
- 资源合并与压缩:将外部脚本、样式合并(webpack进行打包);也可以使用工具进行压缩。
- CSS Spirtes 合并CSS图片
- 使用base64表示简单的图片
- 减少资源体积
- gzip压缩
- 图片/css压缩
- 将外部脚本置底:脚本会阻塞其他资源
- 异步执行inline脚本(script的defer属性)
- css文件放在头部,因为css会阻塞DOM的渲染,但是DOM解析还会继续。
- 懒加载
- 减少重绘和回流操作
- 回流:元素的尺寸、布局、隐藏等属性改变时触发
- 重绘:元素的外观属性改变时触发
3. 浏览器存储
cookie
浏览器每次发请求时都会附加到请求头中发送给服务器。
- 服务器返回响应头Set-Cookie,浏览器根据Set-Cookie以key=value的形式设置cookie
- cookie的常用属性
- expires:cookie的过期时间,格式为GMT时间。默认值为浏览器会话session
- domain:cookie的域名,只有当浏览器域名和cookie域名一致时才能读取到该cookie
- path:控制cookie在当前域名的路径,只有路径匹配才能读取到cookie
- httpOnly 设置能否通过js去访问cookie
// 设置cookie
function setCookie(key, value, exday) {
var d = new Date();
d.setTime(d.getTime() + exday*24*60*60*1000);
document.cookie = key + '=' value + ';expires=' + d.toGMTString();
}
// 获取cookie
function getCookie(key) {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var c = cookies[i].split('=');
if (c[0].trim() == key) {
return c[1];
}
}
return '';
}
localStorage与sessionStorage
- localStorage为持久性存储,没有过期时间,手动删除。解决cookie空间不足的问题。
- localStorage只支持string类型的存储
- localStorage遵循同源策略,不同的网站不能共用localStorage
- sessionStorage为会话型存储,仅当前页面有效,一旦关闭就会被释放。
统一的api来操作和设置数据:
- getItem(key) 获取key对应的本地存储
- setItem(key, value) 设置key的本地存储为value
- removeItem(key) 移除key对应的本地存储
- clear 清空所有本地存储数据
- key(index) 获取对应的key值
一般localStorage存JSON数据,使用JSON.stringify与JSON.parse:
var data = {
name: 'Lily',
age: 20,
sex: 'female'
};
var d = JSON.stringify(data);
localStorage.setItem('data', d);
var jsonData = localStorage.getItem('data');
var obj = JSON.parse(jsonData);
如何实现浏览器多标签页之间的通信?
- 使用localStorage
- 其中一个页面中增删改storage数据
- 其余页面可以监听storage事件,event对象中包含操作的storage对象的key/newValue/oldValue属性。
- storage事件是针对非当前页面对localStorage进行修改时才会触发,当前页面修改时不会触发
- 使用 cookie + setInterval
- 一个页面设置cookie
- 另一个页面轮询setInterval读取cookie
- 通常把cookie的path设置为一个更高级别的目录,从而使更多的页面共享cookie,实现多页面之间相互通信。
4. 移动端开发和PC端开发的区别
主要在于不同分辨率的适配。
- 移动端使用相对大小,如rem,相对HTML根元素的font-size,适配各种移动设备。
- html的font-size是由js计算的,不是css里定义的。一般取100px为参照。或者可以假定100%宽度为7.5rem。
- 先计算body的宽度width = 横向分辨率/100
- 设计稿中的尺寸/100得到css中的尺寸
- dom ready之后,设置html的font-size = deviceWidth/width
- 设置头信息
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
5. 浏览器关键渲染路径的整个过程
- 处理HTML标记数据并生成DOM树
- 处理CSS标记数据并生成CSSOM树
- 将DOM和CSSOM树合并生成渲染树
- 遍历渲染树开始布局,计算各个节点的位置信息
- 确定各个元素的大小以及在视图中的位置
- 将每个节点绘制到屏幕
- 涉及到重绘和回流
css文件:
- css的加载解析不会阻塞DOM的解析
- css的加载解析阻塞DOM的渲染
- css的加载解析阻塞后续js文件的执行
js文件:
- 脚本的加载和执行会阻塞DOM的解析
- 浏览器遇到script(无async和defer)时会触发页面渲染,若之前css尚未加载完毕,会等待css加载完毕再执行脚本
6. this的指向
函数被调用时,调用函数的对象会被传递到执行上下文中,this的值就指向调用函数的对象。
- this的值是在函数调用时确定的,而不是函数定义时。
this的几种情况:
- 普通的函数调用:this指向全局对象,严格模式下为undefined
var m = 'global';
function test() {
console.log(this.m);
}
test(); // 'global'
'use strict'; //严格模式
function test() {
console.log(this);
}
test(); // undefined
- 作为对象方法被调用:this指的是这个对象
var m = 'global';
var obj = {
m: 'obj',
test: function() {
console.log(this.m);
}
};
obj.test(); // 'obj'
// 注意下面这个情况,由于test是在全局中调用的,this指向window
var test = obj.test;
test(); // global
- 作为构造函数调用:构造函数中的this指的是创建的实例对象
function Student() {
this.id = '2012349';
}
var s1 = new Student();
console.log(s1.id); // '2012349'
- 回调函数中的this值
- 由于函数传参是按值传递,若基本数据类型的话就直接复制值传过去,若是引用类型,传递的是该数据在堆中存放的地址
- 回调函数中的this值,决定于回调函数执行的环境
var m = '123';
var obj = {
m: 'obj123',
test: function() {
console.log(this.m);
}
};
setTimeout(obj.test, 1000); // '123'
setTimeout(function() {
obj.test();
}, 1000); // 'obj123'
- 箭头函数:this比较特殊,是定义时上级对象的this,不受调用环境的影响而改变。
var obj = {
m: 1,
test: function() {
console.log(this.m);
return () => {
console.log(this.m);
return () => {
console.log(this.m);
}
};
}
};
obj.test()()(); // 1 1 1
var test = obj.test(); // 1
// 不受调用环境的影响
test()(); // 1 1
改变this指向的方法:
- call/apply
- bind:生成了一个绑定this的新函数
7. 判断数据类型的方法
typeof
一般用来判断基本数据类型,不能用于判断引用类型。只能判断出number/string/boolean/undefined/null/object/function几种,无法区分具体的对象类型。
// 基本数据类型中无法判断null
typeof '123' // string
typeof 123 // number
typeof true // boolean
var a;
typeof a // undefined
typeof null // object
// 可以判断出function
a = function() {}
typeof a // function
// 其余引用类型判断都为object
typeof {} // object
typeof [] // object
instanceof
利用原型链来进行具体对象类别的判断。但是不能区分基本数据类型(包装类new出来的除外)。
// 不能判断基本数据类型
true instanceof Boolean // false
123 instanceof Number // false
null instanceof Object // false
undefined instanceof Object // false
//包装类new的可以判断
var a = new Number(123);
a instanceof Number // true
// 判断数组以及其他的引用类型
[] instanceof Array // true
new Date() instanceof Date // true
Object.prototype.toString.call
返回[object 构造函数名]格式的字符串,不能用于检测非原生的构造函数名。
Object.prototype.toString.call('123') // //[object String]
Object.prototype.toString.call(123) //[object Number]
Object.prototype.toString.call(true) // [object Boolean]
Object.prototype.toString.call(undefined// [object Undefined]
Object.prototype.toString.call(null) // [object Null]
Object.prototype.toString.call([]) // [object Array]
Object.prototype.toString.call({}) // [object Object]
Object.prototype.toString.call(function() {}) // [object Function]
// 非原生的无法判断
function Student(){}
var xiaoming = new Student()
Object.prototype.toString.call(xiaoming) // [object Object]
constructor
使用constructor判断构造函数,但是null/undefined没有constructor属性。并且constructor还可能被手动重写,所以不安全。
var a = true;
a.constructor === Boolean // true
a = 123;
a.constructor === Number // true
a = '123';
a.constructor === String // true
a = [];
a.constructor === Array // true
a = {};
a.constructor === Object // true
a = function() {}
a.constructor === Function // true
function Student(){}
a = new Student()
a.constructor === Student // true
8. js中的继承方式
构造函数继承:
在子类构造函数中调用父类的构造函数(call/apply)
- 子类对象可以访问父类的实例属性和方法
- 子类对象无法访问父类原型对象上的属性
- 对于在构造函数中定义的方法,实例不会共享,造成内存浪费。
function Super(name) {
this.name = name;
this.getName = function() {
return this.name;
}
}
Super.prototype.sex = 'male';
function Sub(name, age) {
Super.call(this, name);
this.age = age;
this.getAge = function() {
return this.age;
}
}
var son1 = new Sub('lili', 10);
var son2 = new Sub('mary', 18);
console.log(son1.name); // lili
console.log(son2.sex); // undefined
console.log(son1.getName === son2.getName); // false
优点:
- 可以实现多继承
- 可以向父类构造函数中传递参数
缺点
- 无法继承父类原型上的方法和属性
- 每个对象都各自添加了父类中的属性和方法,父类方法无法复用,占用内存空间
原型链继承:
将父类的实例作为子类的原型。
function Super(name) {
this.name = name;
this.getName = function() {
return this.name;
}
}
Super.prototype.sex = 'male';
Super.prototype.run = function() {
console.log('in Super prototype run');
};
function Sub(age) {
this.age = age;
this.getAge = function() {
return this.age;
}
}
Sub.prototype = new Super('父类');
Sub.prototype.constructor = Sub;
var son1 = new Sub(12);
var son2 = new Sub(15);
son1.sex = 'female';
console.log(son2.sex); // female
优点:
- 子类可以访问到父类及其原型上的公共方法和属性(原型上的方法是复用的)
缺点:
- 无法实现多继承
- 无法给父类构造函数传递参数(即使传递了对于每个子类该属性都是一致的)
- 给子类原型添加属性或方法要在Sub.prototype = new Super()之后,防止被覆盖。
- 原型上的属性被其中一个实例修改了之后,所有实例都会改变
组合继承:构造函数+原型链
- 使用构造函数继承父类的实例属性
- 使用原型继承父类的公共属性和方法
function Super(name) {
this.name = name;
}
Super.prototype.getName = function() {
return this.name;
}
function Sub(name, age) {
Super.call(this, name);
this.age = age;
this.getAge = function() {
return this.age;
}
}
Sub.prototype = new Super();
Sub.prototype.constructor = Sub;
优点:
- 可以向父类构造函数中传递参数
- 父类公共方法的复用
缺点:
- 无法实现多继承
- 调用两次父类构造函数
注:可以通过Object.assign实现多继承
function Sub() {
Super.call(this);
}
Sub.prototype = Object.create(Super.prototype);
Object.assign(Sub.prototype, One.prototype, Two.prototype);
Sub.prototype.constructor = Sub;
9. 事件委托/代理
事件委托就是只指定一个事件处理程序,来管理某一类型的所有事件。事件委托的原理是事件冒泡,利用event.target/srcElement来判断是否为目标节点
- 事件触发后,会在子元素与父元素之间传播,DOM事件流共分为 捕获阶段、目标阶段、冒泡阶段
- 捕获:从document到目标节点
- 目标阶段:在目标节点上触发
- 冒泡:从目标节点到document
- 使用addEventListener来注册事件,第三个参数useCapture为true时表示在捕获阶段触发事件,为false时表示在冒泡阶段触发事件。
- IE中使用attachEvent注册事件,默认是冒泡
- onclick等类型注册的事件只会有一个生效
- 阻止冒泡:stopPropagation()或者是cancelBubble = true
事件委托的优势:
- 减少事件注册,节省内存占用
- 新增子元素时无需对其绑定事件(动态绑定效果)
window.onload = function() {
let ul = document.getElementById('ul');
ul.addEventListener('click', function() {
if (e.target.nodeName.toLowerCase() === 'li') {
console.log(e.target.innerHtml);
}
}, false);
}
不支持冒泡的事件:
- blur / focus
- mouseenter / mouseleave
- load / unload/ scroll / resize
在一个DOM上同时绑定两个点击事件:一个用捕获,一个用冒泡。事件会执行几次,先执行冒泡还是捕获。
- 目标节点上的事件按照绑定顺序依次执行,不管是冒泡还是捕获。
<div class="box">测试</div>
var box = document.querySelector('.box');
box.addEventListener('click', function() {
console.log('bubble box');
}, false);
box.addEventListener('click', function() {
console.log('capture box');
}, true);
// bubble box
// capture box
var box = document.querySelector('.box');
box.addEventListener('click', function() {
console.log('capture box');
}, true);
box.addEventListener('click', function() {
console.log('bubble box');
}, false);
// capture box
// bubble box
- 事件在父子之间传播时,除目标节点外,其余都是先捕获后冒泡。
- 其它元素捕获阶段事件 -> 目标元素代码顺序事件 -> 其它元素冒泡阶段事件
<div class="box">
<button class="btn">点击</button>
</div>
var box = document.querySelector('.box');
var btn = document.querySelector('.btn');
box.addEventListener('click', function() {
console.log('capture box');
}, true);
box.addEventListener('click', function() {
console.log('bubble box');
}, false);
btn.addEventListener('click', function() {
console.log('capture btn');
}, true);
btn.addEventListener('click', function() {
console.log('bubble btn');
}, false);
// capture box
// capture btn
// bubble btn
// bubble box
10. DocumentFragment
文档片段接口,表示一个没有父级文件的最小文档对象。作为一个轻量版的document使用。
- DocumentFragment不属于真实DOM树,它的变化不会触发重排,没有性能问题。
- 当需要插入多个DOM节点时,可以先添加到DocumentFragment中,一次性添加到文档中。(插入的是片段的所有子节点,不是片段本身)
- document.createDocumentFragment() 创建一个新的文档片段
// 向一个ul中增加多个li
var element = document.querySelector('ul');
var fragment = document.createDocumentFragment();
var i = 0;
while(i < 10) {
var li = document.createElement('li');
li.innerHTML = i;
fragment.appendChild(li);
}
element.appendChild(fragment);