秋招前端面经总结-1

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);

你可能感兴趣的:(前端,秋招,面经)