2023最新前端面试总结

介绍 plugin

插件系统是 Webpack 成功的一个关键性因素。在编译的整个生命周期中,Webpack 会触发许多事件钩子,Plugin 可以监听这些事件,根据需求在相应的时间点对打包内容进行定向的修改。

一个最简单的 plugin 是这样的:

class Plugin{
      // 注册插件时,会调用 apply 方法
      // apply 方法接收 compiler 对象
      // 通过 compiler 上提供的 Api,可以对事件进行监听,执行相应的操作
      apply(compiler){
          // compilation 是监听每次编译循环
          // 每次文件变化,都会生成新的 compilation 对象并触发该事件
        compiler.plugin('compilation',function(compilation) {})
      }
}

注册插件:

// webpack.config.js
module.export = {
    plugins:[
        new Plugin(options),
    ]
}

事件流机制:

Webpack 就像工厂中的一条产品流水线。原材料经过 Loader 与 Plugin 的一道道处理,最后输出结果。
  • 通过链式调用,按顺序串起一个个 Loader;
  • 通过事件流机制,让 Plugin 可以插入到整个生产过程中的每个步骤中;
Webpack 事件流编程范式的核心是基础类 Tapable,是一种 观察者模式 的实现事件的订阅与广播:
const { SyncHook } = require("tapable")

const hook = new SyncHook(['arg'])

// 订阅
hook.tap('event', (arg) => {
    // 'event-hook'
    console.log(arg)
})

// 广播
hook.call('event-hook')
Webpack 中两个最重要的类 CompilerCompilation 便是继承于 Tapable,也拥有这样的事件流机制。
  • Compiler : 可以简单的理解为 Webpack 实例,它包含了当前 Webpack 中的所有配置信息,如 options, loaders, plugins 等信息,全局唯一,只在启动时完成初始化创建,随着生命周期逐一传递;
  • Compilation: 可以称为 编译实例。当监听到文件发生改变时,Webpack 会创建一个新的 Compilation 对象,开始一次新的编译。它包含了当前的输入资源,输出资源,变化的文件等,同时通过它提供的 api,可以监听每次编译过程中触发的事件钩子;
  • 区别:

    • Compiler 全局唯一,且从启动生存到结束;
    • Compilation对应每次编译,每轮编译循环均会重新创建;
  • 常用 Plugin:

    • UglifyJsPlugin: 压缩、混淆代码;
    • CommonsChunkPlugin: 代码分割;
    • ProvidePlugin: 自动加载模块;
    • html-webpack-plugin: 加载 html 文件,并引入 css / js 文件;
    • extract-text-webpack-plugin / mini-css-extract-plugin: 抽离样式,生成 css 文件; DefinePlugin: 定义全局变量;
    • optimize-css-assets-webpack-plugin: CSS 代码去重;
    • webpack-bundle-analyzer: 代码分析;
    • compression-webpack-plugin: 使用 gzip 压缩 js 和 css;
    • happypack: 使用多进程,加速代码构建;
    • EnvironmentPlugin: 定义环境变量;
  • 调用插件 apply 函数传入 compiler 对象
  • 通过 compiler 对象监听事件

loader和plugin有什么区别?

webapck默认只能打包JS和JOSN模块,要打包其它模块,需要借助loader,loader就可以让模块中的内容转化成webpack或其它laoder可以识别的内容。
  • loader就是模块转换化,或叫加载器。不同的文件,需要不同的loader来处理。
  • plugin是插件,可以参与到整个webpack打包的流程中,不同的插件,在合适的时机,可以做不同的事件。

webpack中都有哪些插件,这些插件有什么作用?

  • html-webpack-plugin 自动创建一个HTML文件,并把打包好的JS插入到HTML文件中
  • clean-webpack-plugin 在每一次打包之前,删除整个输出文件夹下所有的内容
  • mini-css-extrcat-plugin 抽离CSS代码,放到一个单独的文件中
  • optimize-css-assets-plugin 压缩css

用过 TypeScript 吗?它的作用是什么?

为 JS 添加类型支持,以及提供最新版的 ES 语法的支持,是的利于团队协作和排错,开发大型项目

Cookie有哪些字段,作用分别是什么

Cookie由以下字段组成:

  • Name:cookie的名称
  • Value:cookie的值,对于认证cookie,value值包括web服务器所提供的访问令牌;
  • Size: cookie的大小
  • Path:可以访问此cookie的页面路径。 比如domain是abc.com,path是/test,那么只有/test路径下的页面可以读取此cookie。
  • Secure: 指定是否使用HTTPS安全协议发送Cookie。使用HTTPS安全协议,可以保护Cookie在浏览器和Web服务器间的传输过程中不被窃取和篡改。该方法也可用于Web站点的身份鉴别,即在HTTPS的连接建立阶段,浏览器会检查Web网站的SSL证书的有效性。但是基于兼容性的原因(比如有些网站使用自签署的证书)在检测到SSL证书无效时,浏览器并不会立即终止用户的连接请求,而是显示安全风险信息,用户仍可以选择继续访问该站点。
  • Domain:可以访问该cookie的域名,Cookie 机制并未遵循严格的同源策略,允许一个子域可以设置或获取其父域的 Cookie。当需要实现单点登录方案时,Cookie 的上述特性非常有用,然而也增加了 Cookie受攻击的危险,比如攻击者可以借此发动会话定置攻击。因而,浏览器禁止在 Domain 属性中设置.org、.com 等通用顶级域名、以及在国家及地区顶级域下注册的二级域名,以减小攻击发生的范围。
  • HTTP: 该字段包含HTTPOnly 属性 ,该属性用来设置cookie能否通过脚本来访问,默认为空,即可以通过脚本访问。在客户端是不能通过js代码去设置一个httpOnly类型的cookie的,这种类型的cookie只能通过服务端来设置。该属性用于防止客户端脚本通过document.cookie属性访问Cookie,有助于保护Cookie不被跨站脚本攻击窃取或篡改。但是,HTTPOnly的应用仍存在局限性,一些浏览器可以阻止客户端脚本对Cookie的读操作,但允许写操作;此外大多数浏览器仍允许通过XMLHTTP对象读取HTTP响应中的Set-Cookie头。
  • Expires/Max-size : 此cookie的超时时间。若设置其值为一个时间,那么当到达此时间后,此cookie失效。不设置的话默认值是Session,意思是cookie会和session一起失效。当浏览器关闭(不是浏览器标签页,而是整个浏览器) 后,此cookie失效。

总结: 服务器端可以使用 Set-Cookie 的响应头部来配置 cookie 信息。一条cookie 包括了5个属性值 expires、domain、path、secure、HttpOnly。其中 expires 指定了 cookie 失效的时间,domain 是域名、path是路径,domain 和 path 一起限制了 cookie 能够被哪些 url 访问。secure 规定了 cookie 只能在确保安全的情况下传输,HttpOnly 规定了这个 cookie 只能被服务器访问,不能使用 js 脚本访问。

JS 隐式转换,显示转换

一般非基础类型进行转换时会先调用 valueOf,如果 valueOf 无法返回基本类型值,就会调用 toString

字符串和数字

  • "+" 操作符,如果有一个为字符串,那么都转化到字符串然后执行字符串拼接
  • "-" 操作符,转换为数字,相减 (-a, a * 1 a/1) 都能进行隐式强制类型转换
[] + {} 和 {} + []

布尔值到数字

  • 1 + true = 2
  • 1 + false = 1

转换为布尔值

  • for 中第二个
  • while
  • if
  • 三元表达式
  • || (逻辑或) && (逻辑与)左边的操作数

符号

  • 不能被转换为数字
  • 能被转换为布尔值(都是 true)
  • 可以被转换成字符串 "Symbol(cool)"

宽松相等和严格相等

宽松相等允许进行强制类型转换,而严格相等不允许

字符串与数字

转换为数字然后比较

其他类型与布尔类型

  • 先把布尔类型转换为数字,然后继续进行比较

对象与非对象

  • 执行对象的 ToPrimitive(对象)然后继续进行比较

假值列表

  • undefined
  • null
  • false
  • +0, -0, NaN
  • ""

数组扁平化

ES5 递归写法 —— isArray()、concat()

function flat11(arr) {
    var res = [];
    for (var i = 0; i < arr.length; i++) {
        if (Array.isArray(arr[i])) {
            res = res.concat(flat11(arr[i]));
        } else {
            res.push(arr[i]);
        }
    }
    return res;
}

如果想实现第二个参数(指定“拉平”的层数),可以这样实现,后面的几种可以自己类似实现:

function flat(arr, level = 1) {
    var res = [];
    for(var i = 0; i < arr.length; i++) {
        if(Array.isArray(arr[i]) || level >= 1) {
            res = res.concat(flat(arr[i]), level - 1);
        }
        else {
            res.push(arr[i]);
        }
    }
    return res;
}

ES6 递归写法 — reduce()、concat()、isArray()

function flat(arr) {
    return arr.reduce(
        (pre, cur) => pre.concat(Array.isArray(cur) ? flat(cur) : cur), []
    );
}

ES6 迭代写法 — 扩展运算符(...)、some()、concat()、isArray()

ES6 的扩展运算符(...) 只能扁平化一层

function flat(arr) {
    return [].concat(...arr);
}

全部扁平化:遍历原数组,若arr中含有数组则使用一次扩展运算符,直至没有为止。

function flat(arr) {
    while(arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr);
    }
    return arr;
}

toString/join & split

调用数组的 toString()/join() 方法(它会自动扁平化处理),将数组变为字符串然后再用 split 分割还原为数组。由于 split 分割后形成的数组的每一项值为字符串,所以需要用一个map方法遍历数组将其每一项转换为数值型。

function flat(arr){
    return arr.toString().split(',').map(item => Number(item));
    // return arr.join().split(',').map(item => Number(item));
}

使用正则

JSON.stringify(arr).replace(/[|]/g, '') 会先将数组arr序列化为字符串,然后使用 replace() 方法将字符串中所有的[] 替换成空字符,从而达到扁平化处理,此时的结果为 arr 不包含 [] 的字符串。最后通过JSON.parse() 解析字符串。

function flat(arr) {
    return JSON.parse("[" + JSON.stringify(arr).replace(/\[|\]/g,'') + "]");
}

类数组转化为数组

类数组是具有 length 属性,但不具有数组原型上的方法。常见的类数组有 arguments、DOM 操作方法返回的结果(如document.querySelectorAll('div'))等。

扩展运算符(...)

注意:扩展运算符只能作用于 iterable 对象,即拥有 Symbol(Symbol.iterator) 属性值。

let arr = [...arrayLike]

Array.from()

let arr = Array.from(arrayLike);

Array.prototype.slice.call()

let arr = Array.prototype.slice.call(arrayLike);

Array.apply()

let arr = Array.apply(null, arrayLike);

concat + apply

let arr = Array.prototype.concat.apply([], arrayLike);

代码输出结果

function runAsync (x) {
  const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
  return p
}
Promise.race([runAsync(1), runAsync(2), runAsync(3)])
  .then(res => console.log('result: ', res))
  .catch(err => console.log(err))

输出结果如下:

1
'result: ' 1
2
3

then只会捕获第一个成功的方法,其他的函数虽然还会继续执行,但是不是被then捕获了。

参考 前端进阶面试题详细解答

实现节流函数和防抖函数

函数防抖的实现:

function debounce(fn, wait) {
  var timer = null;

  return function() {
    var context = this,
      args = [...arguments];

    // 如果此时存在定时器的话,则取消之前的定时器重新记时
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }

    // 设置定时器,使事件间隔指定事件后执行
    timer = setTimeout(() => {
      fn.apply(context, args);
    }, wait);
  };
}

函数节流的实现:

// 时间戳版
function throttle(fn, delay) {
  var preTime = Date.now();

  return function() {
    var context = this,
      args = [...arguments],
      nowTime = Date.now();

    // 如果两次时间间隔超过了指定时间,则执行函数。
    if (nowTime - preTime >= delay) {
      preTime = Date.now();
      return fn.apply(context, args);
    }
  };
}

// 定时器版
function throttle (fun, wait){
  let timeout = null
  return function(){
    let context = this
    let args = [...arguments]
    if(!timeout){
      timeout = setTimeout(() => {
        fun.apply(context, args)
        timeout = null 
      }, wait)
    }
  }
}

如何判断元素是否到达可视区域

以图片显示为例:

  • window.innerHeight 是浏览器可视区的高度;
  • document.body.scrollTop || document.documentElement.scrollTop 是浏览器滚动的过的距离;
  • imgs.offsetTop 是元素顶部距离文档顶部的高度(包括滚动条的距离);
  • 内容达到显示区域的:img.offsetTop < window.innerHeight + document.body.scrollTop;

map和weakMap的区别

(1)Map map本质上就是键值对的集合,但是普通的Object中的键值对中的键只能是字符串。而ES6提供的Map数据结构类似于对象,但是它的键不限制范围,可以是任意类型,是一种更加完善的Hash结构。如果Map的键是一个原始数据类型,只要两个键严格相同,就视为是同一个键。

实际上Map是一个数组,它的每一个数据也都是一个数组,其形式如下:

const map = [
     ["name","张三"],
     ["age",18],
]

Map数据结构有以下操作方法:

  • sizemap.size 返回Map结构的成员总数。
  • set(key,value):设置键名key对应的键值value,然后返回整个Map结构,如果key已经有值,则键值会被更新,否则就新生成该键。(因为返回的是当前Map对象,所以可以链式调用)
  • get(key):该方法读取key对应的键值,如果找不到key,返回undefined。
  • has(key):该方法返回一个布尔值,表示某个键是否在当前Map对象中。
  • delete(key):该方法删除某个键,返回true,如果删除失败,返回false。
  • clear():map.clear()清除所有成员,没有返回值。

Map结构原生提供是三个遍历器生成函数和一个遍历方法

  • keys():返回键名的遍历器。
  • values():返回键值的遍历器。
  • entries():返回所有成员的遍历器。
  • forEach():遍历Map的所有成员。
const map = new Map([
     ["foo",1],
     ["bar",2],
])
for(let key of map.keys()){
    console.log(key);  // foo bar
}
for(let value of map.values()){
     console.log(value); // 1 2
}
for(let items of map.entries()){
    console.log(items);  // ["foo",1]  ["bar",2]
}
map.forEach( (value,key,map) => {
     console.log(key,value); // foo 1    bar 2
})

(2)WeakMap WeakMap 对象也是一组键值对的集合,其中的键是弱引用的。其键必须是对象,原始数据类型不能作为key值,而值可以是任意的。

该对象也有以下几种方法:

  • set(key,value):设置键名key对应的键值value,然后返回整个Map结构,如果key已经有值,则键值会被更新,否则就新生成该键。(因为返回的是当前Map对象,所以可以链式调用)
  • get(key):该方法读取key对应的键值,如果找不到key,返回undefined。
  • has(key):该方法返回一个布尔值,表示某个键是否在当前Map对象中。
  • delete(key):该方法删除某个键,返回true,如果删除失败,返回false。

其clear()方法已经被弃用,所以可以通过创建一个空的WeakMap并替换原对象来实现清除。

WeakMap的设计目的在于,有时想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。一旦不再需要这两个对象,就必须手动删除这个引用,否则垃圾回收机制就不会释放对象占用的内存。

而WeakMap的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用

总结:

  • Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
  • WeakMap 结构与 Map 结构类似,也是用于生成键值对的集合。但是 WeakMap 只接受对象作为键名( null 除外),不接受其他类型的值作为键名。而且 WeakMap 的键名所指向的对象,不计入垃圾回收机制。

代码输出问题

function Parent() {
    this.a = 1;
    this.b = [1, 2, this.a];
    this.c = { demo: 5 };
    this.show = function () {
        console.log(this.a , this.b , this.c.demo );
    }
}

function Child() {
    this.a = 2;
    this.change = function () {
        this.b.push(this.a);
        this.a = this.b.length;
        this.c.demo = this.a++;
    }
}

Child.prototype = new Parent();
var parent = new Parent();
var child1 = new Child();
var child2 = new Child();
child1.a = 11;
child2.a = 12;
parent.show();
child1.show();
child2.show();
child1.change();
child2.change();
parent.show();
child1.show();
child2.show();

输出结果:

parent.show(); // 1  [1,2,1] 5

child1.show(); // 11 [1,2,1] 5
child2.show(); // 12 [1,2,1] 5

parent.show(); // 1 [1,2,1] 5

child1.show(); // 5 [1,2,1,11,12] 5

child2.show(); // 6 [1,2,1,11,12] 5

这道题目值得神帝,他涉及到的知识点很多,例如this的指向、原型、原型链、类的继承、数据类型等。

解析:

  1. parent.show(),可以直接获得所需的值,没啥好说的;
  2. child1.show(),Child的构造函数原本是指向Child的,题目显式将Child类的原型对象指向了Parent类的一个实例,需要注意Child.prototype指向的是Parent的实例parent,而不是指向Parent这个类。
  3. child2.show(),这个也没啥好说的;
  4. parent.show(),parent是一个Parent类的实例,Child.prorotype指向的是Parent类的另一个实例,两者在堆内存中互不影响,所以上述操作不影响parent实例,所以输出结果不变;
  5. child1.show(),child1执行了change()方法后,发生了怎样的变化呢?
  6. this.b.push(this.a),由于this的动态指向特性,this.b会指向Child.prototype上的b数组,this.a会指向child1a属性,所以Child.prototype.b变成了[1,2,1,11];
  7. this.a = this.b.length,这条语句中this.athis.b的指向与上一句一致,故结果为child1.a变为4;
  8. this.c.demo = this.a++,由于child1自身属性并没有c这个属性,所以此处的this.c会指向Child.prototype.cthis.a值为4,为原始类型,故赋值操作时会直接赋值,Child.prototype.c.demo的结果为4,而this.a随后自增为5(4 + 1 = 5)。
  9. child2执行了change()方法, 而child2child1均是Child类的实例,所以他们的原型链指向同一个原型对象Child.prototype,也就是同一个parent实例,所以child2.change()中所有影响到原型对象的语句都会影响child1的最终输出结果。
  10. this.b.push(this.a),由于this的动态指向特性,this.b会指向Child.prototype上的b数组,this.a会指向child2a属性,所以Child.prototype.b变成了[1,2,1,11,12];
  11. this.a = this.b.length,这条语句中this.athis.b的指向与上一句一致,故结果为child2.a变为5;
  12. this.c.demo = this.a++,由于child2自身属性并没有c这个属性,所以此处的this.c会指向Child.prototype.c,故执行结果为Child.prototype.c.demo的值变为child2.a的值5,而child2.a最终自增为6(5 + 1 = 6)。

Promise.all和Promise.race的区别的使用场景

(1)Promise.all Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值

Promise.all中传入的是数组,返回的也是是数组,并且会将进行映射,传入的promise对象返回的值是按照顺序在数组中排列的,但是注意的是他们执行的顺序并不是按照顺序的,除非可迭代对象为空。

需要注意,Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,这样当遇到发送多个请求并根据请求顺序获取和使用数据的场景,就可以使用Promise.all来解决。

(2)Promise.race

顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。当要做一件事,超过多长时间就不做了,可以用这个方法来解决:

Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})

GET和POST的请求的区别

Post 和 Get 是 HTTP 请求的两种方法,其区别如下:

  • 应用场景: GET 请求是一个幂等的请求,一般 Get 请求用于对服务器资源不会产生影响的场景,比如说请求一个网页的资源。而 Post 不是一个幂等的请求,一般用于对服务器资源会产生影响的情景,比如注册用户这一类的操作。
  • 是否缓存: 因为两者应用场景不同,浏览器一般会对 Get 请求缓存,但很少对 Post 请求缓存。
  • 发送的报文格式: Get 请求的报文中实体部分为空,Post 请求的报文中实体部分一般为向服务器发送的数据。
  • 安全性: Get 请求可以将请求的参数放入 url 中向服务器发送,这样的做法相对于 Post 请求来说是不太安全的,因为请求的 url 会被保留在历史记录中。
  • 请求长度: 浏览器由于对 url 长度的限制,所以会影响 get 请求发送数据时的长度。这个限制是浏览器规定的,并不是 RFC 规定的。
  • 参数类型: post 的参数传递支持更多的数据类型。

Vue通信

1.props和$emit
2.中央事件总线 EventBus(基本不用)
3.vuex(官方推荐状态管理器)
4.$parent和$children
当然还有一些其他办法,但基本不常用,或者用起来太复杂来。 介绍来通信的方式,还可以扩展说一下使用
场景,如何使用,注意事项之类的。

HTTP2的头部压缩算法是怎样的?

HTTP2的头部压缩是HPACK算法。在客户端和服务器两端建立“字典”,用索引号表示重复的字符串,采用哈夫曼编码来压缩整数和字符串,可以达到50%~90%的高压缩率。

具体来说:

  • 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键值对,对于相同的数据,不再通过每次请求和响应发送;
  • 首部表在HTTP/2的连接存续期内始终存在,由客户端和服务器共同渐进地更新;
  • 每个新的首部键值对要么被追加到当前表的末尾,要么替换表中之前的值。

例如下图中的两个请求, 请求一发送了所有的头部字段,第二个请求则只需要发送差异数据,这样可以减少冗余数据,降低开销。

渲染机制

1. 浏览器如何渲染网页

概述:浏览器渲染一共有五步

  1. 处理 HTML 并构建 DOM 树。
  2. 处理 CSS构建 CSSOM 树。
  3. DOMCSSOM 合并成一个渲染树。
  4. 根据渲染树来布局,计算每个节点的位置。
  5. 调用 GPU 绘制,合成图层,显示在屏幕上
第四步和第五步是最耗时的部分,这两步合起来,就是我们通常所说的渲染

具体如下图过程如下图所示

img

img

渲染

  • 网页生成的时候,至少会渲染一次
  • 在用户访问的过程中,还会不断重新渲染
重新渲染需要重复之前的第四步(重新生成布局)+第五步(重新绘制)或者只有第五个步(重新绘制)
  • 在构建 CSSOM 树时,会阻塞渲染,直至 CSSOM树构建完成。并且构建 CSSOM 树是一个十分消耗性能的过程,所以应该尽量保证层级扁平,减少过度层叠,越是具体的 CSS 选择器,执行速度越慢
  • HTML 解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件。并且CSS也会影响 JS 的执行,只有当解析完样式表才会执行 JS,所以也可以认为这种情况下,CSS 也会暂停构建 DOM

2. 浏览器渲染五个阶段

2.1 第一步:解析HTML标签,构建DOM树

在这个阶段,引擎开始解析html,解析出来的结果会成为一棵domdom的目的至少有2
  • 作为下个阶段渲染树状图的输入
  • 成为网页和脚本的交互界面。(最常用的就是getElementById等等)

当解析器到达script标签的时候,发生下面四件事情

  1. html解析器停止解析,
  2. 如果是外部脚本,就从外部网络获取脚本代码
  3. 将控制权交给js引擎,执行js代码
  4. 恢复html解析器的控制权
由此可以得到第一个结论1
  • 由于
    • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
    • DOM 离线后修改,比如:先把 DOMdisplay:none (有一次 Reflow),然后你修改100次,然后再把它显示出来
    • 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量
    for(let i = 0; i < 1000; i++) {
        // 获取 offsetTop 会导致回流,因为需要去获取正确的值
        console.log(document.querySelector('.test').style.offsetTop)
    }
    • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
    • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
    • CSS选择符从右往左匹配查找,避免 DOM深度过深
    • 将频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于 video标签,浏览器会自动将该节点变为图层。

    img

    img

    说一说什么是跨域,怎么解决

    因为浏览器出于安全考虑,有同源策略。也就是说,如果协议、域名或者端口有一个不同就是跨域,Ajax 请求会失败。
    为来防止CSRF攻击
    1.JSONP
        JSONP 的原理很简单,就是利用 
        
        JSONP 使用简单且兼容性不错,但是只限于 get 请求。
    2.CORS
        CORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现。
    3.document.domain
        该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。
    
        只需要给页面添加 document.domain = 'test.com' 表示二级域名都相同就可以实现跨域
    4.webpack配置proxyTable设置开发环境跨域
    5.nginx代理跨域
    6.iframe跨域
    7.postMessage
        这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息
    

    let、const、var的区别

    (1)块级作用域: 块作用域由 { }包括,let和const具有块级作用域,var不存在块级作用域。块级作用域解决了ES5中的两个问题:

    • 内层变量可能覆盖外层变量
    • 用来计数的循环变量泄露为全局变量

    (2)变量提升: var存在变量提升,let和const不存在变量提升,即在变量只能在声明之后使用,否在会报错。

    (3)给全局添加属性: 浏览器的全局对象是window,Node的全局对象是global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会。

    (4)重复声明: var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。

    (5)暂时性死区: 在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。

    (6)初始值设置: 在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。

    (7)指针指向: let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。

    区别 var let const
    是否有块级作用域 × ✔️ ✔️
    是否存在变量提升 ✔️ × ×
    是否添加全局属性 ✔️ × ×
    能否重复声明变量 ✔️ × ×
    是否存在暂时性死区 × ✔️ ✔️
    是否必须设置初始值 × × ✔️
    能否改变指针指向 ✔️ ✔️ ×

    BFC

    块级格式化上下文,是一个独立的渲染区域,让处于 BFC 内部的元素与外部的元素相互隔离,使内外元素的定位不会相互影响。

    IE下为 Layout,可通过 zoom:1 触发

    触发条件:

    • 根元素
    • position: absolute/fixed
    • display: inline-block / table
    • float 元素
    • ovevflow !== visible

    规则:

    • 属于同一个 BFC 的两个相邻 Box 垂直排列
    • 属于同一个 BFC 的两个相邻 Boxmargin 会发生重叠
    • BFC 中子元素的 margin box 的左边, 与包含块 (BFC) border box的左边相接触 (子元素 absolute 除外)
    • BFC 的区域不会与 float 的元素区域重叠
    • 计算 BFC 的高度时,浮动子元素也参与计算
    • 文字层不会被浮动层覆盖,环绕于周围

    应用:

    • 阻止margin重叠
    • 可以包含浮动元素 —— 清除内部浮动(清除浮动的原理是两个div都位于同一个 BFC 区域之中)
    • 自适应两栏布局
    • 可以阻止元素被浮动元素覆盖

    上下垂直居中方案

    • 定高:marginposition + margin(负值)
    • 不定高:position + transformflexIFC + vertical-align:middle
    /* 定高方案1 */
    .center {
      height: 100px;
      margin: 50px 0;   
    }
    /* 定高方案2 */
    .center {
      height: 100px;
      position: absolute;
      top: 50%;
      margin-top: -25px;
    }
    /* 不定高方案1 */
    .center {
      position: absolute;
      top: 50%;
      transform: translateY(-50%);
    }
    /* 不定高方案2 */
    .wrap {
      display: flex;
      align-items: center;
    }
    .center {
      width: 100%;
    }
    /* 不定高方案3 */
    /* 设置 inline-block 则会在外层产生 IFC,高度设为 100% 撑开 wrap 的高度 */
    .wrap::before {
      content: '';
      height: 100%;
      display: inline-block;
      vertical-align: middle;
    }
    .wrap {
      text-align: center;
    }
    .center {
      display: inline-block;  
      vertical-align: middle;
    }

    二分查找--时间复杂度 log2(n)

    题目描述:如何确定一个数在一个有序数组中的位置

    实现代码如下:

    function search(arr, target, start, end) {
      let targetIndex = -1;
    
      let mid = Math.floor((start + end) / 2);
    
      if (arr[mid] === target) {
        targetIndex = mid;
        return targetIndex;
      }
    
      if (start >= end) {
        return targetIndex;
      }
    
      if (arr[mid] < target) {
        return search(arr, target, mid + 1, end);
      } else {
        return search(arr, target, start, mid - 1);
      }
    }
    // const dataArr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
    // const position = search(dataArr, 6, 0, dataArr.length - 1);
    // if (position !== -1) {
    //   console.log(`目标元素在数组中的位置:${position}`);
    // } else {
    //   console.log("目标元素不在数组中");
    // }
    

你可能感兴趣的:(前端javascript)