Fork me on GitHub

作用域和闭包

# 专题总结:作用域和闭包

拿到 字节跳动实习生 offer 总结

回馈分享一波自己的知识点总结

希望读者依此构建自己的知识树(思维导图)

偷懒一下:可参考我自己总结思维导图 : 点这里

附带:高频面试题积累文档。 来自于(学长、牛客网等平台)

自己开发的博客地址:zxinc520.com

github 地址: 点击

此篇 js - 【作用域和闭包】 知识点: 全部弄懂了,面试很容易。

# 一、作用域和作用域链概念

# 1.1、作用域

# 1.1.1、作用域是什么

作用域本质就是程序源代码中定义变量的区域,它可以解释为一套规则,是关于 JS 引擎如何寻找变量以及会在何处找到变量的规则。

# 1.1.2、作用域分为哪些

  • 词法作用域(静态作用域)
    • 词法作用域是在写代码时就确定了作用域(不使用 eval 和 with 的前提下,这两个现在基本不使用,因此不讲了),即静态作用域
  • 动态作用域
    • 而动态作用域是在代码运行时动态确定的

# 1.2、作用域链

# 1.2.1、作用域链是什么

作用域链实际上是指向变量对象的指针列表,它只引用但不实际包含变量对象,它的用途是保证对执行环境有权访问的所有变量和函数的有序访问。

简单来说:作用域链就是从当前作用域开始一层一层向上寻找某个变量,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是作用域链。

# 两个重要概念:
  • 变量对象

    每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中(变量对象其实就是作用域这个抽象概念的具体值),比如一个函数中包含的局部变量,它的参数,它里面声明的函数都存在变量对象中。(一个当前执行函数的变量对象最开始时就包含一个 arguments 对象,这个对象用来装函数括号内的参数,所以全局环境的变量对象没有这个)

  • 执行环境

    也可以叫执行上下文,这里定义了变量或函数有权访问的其他数据,当一个函数被执行时,他的执行环境会被推入环境栈,执行之后才会被弹出,把控制权返回给之前的执行环境。

# 二、闭包

# 2.1、闭包是什么

闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包最常用的方式就是在一个函数内部创建另一个函数。

# 2.2、闭包的作用

  1. 能够访问函数定义时所在的词法作用域 (阻止其被回收)。

  2. 私有化变量

  3. 模拟块级作用域

  4. 创建模块

    • 两个必备的条件 (来自《你不知道的 JavaScript》)
      • 必须有外部的封闭函数,该函数必须至少被调用一次 (每次调用都会创建一个新的模块实例)
      • 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
    function coolModule() {
      let name = "Yvette";
      let age = 20;
      function sayName() {
        console.log(name);
      }
      function sayAge() {
        console.log(age);
      }
      return {
        sayName,
        sayAge,
      };
    }
    let info = coolModule();
    info.sayName(); //'Yvette'

# 2.3、闭包的缺点

闭包会导致函数的变量一直保存在内存中,过多的闭包可能会导致内存泄漏

2.4、经典题目(闭包)

函数自增

var fn = (function () {
  let i = 0;
  return function () {
    return i++;
  };
})();
console.log(fn()); //0
console.log(fn()); //1
console.log(fn()); //2
console.log(fn()); //3

原型和原型链

# JavaScript 原型和原型链

拿到 字节跳动实习生 offer 总结

回馈分享一波自己的知识点总结

希望读者依此构建自己的知识树(思维导图)

偷懒一下:可参考我自己总结思维导图 : 点这里

附带:高频面试题积累文档。 来自于(学长、牛客网等平台)

自己开发的博客地址:zxinc520.com

github 地址: 点击

此篇 js - 【原型和原型链】 知识点: 全部弄懂了,面试很容易。

# 一、原型和原型定义

# 1.1、背景

JavaScript 中除了基础类型外的数据类型,都是对象(引用类型)。但是由于其没有 类(class,ES6 引入了 class,但其只是语法糖)的概念,如何将所有对象联系起来就成立一个问题,于是就有了原型和原型链的概念。

# 1.2、原型是什么?

原型是一个 prototype 对象,用于表示对象之间的关系。

# 1.3、原型链

每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象 ( proto ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。以上一整个原型与原型层层相链接的过程即为原型链

# 1.4、公式

var 对象 = new 函数();
对象.__proto__ === 对象的构造函数.prototype;

# 二、7 大继承写法

常考点【熟练掌握】

# 2.1、原型链继承

  • 原型链继承的基本思想:是利用原型让一个引用类型继承另一个引用类型的属性和方法。

    如 SubType.prototype = new SuperType ();

    function SuperType() {
      this.name = "Yvette";
    }
    function SubType() {
      this.age = 22;
    }
    SubType.prototype = new SuperType();
  • 缺点

    1. 通过原型来实现继承时,原型会变成另一个类型的实例,原先的实例属性变成了现在的原型属性,该原型的引用类型属性会被所有的实例共享
    2. 在创建子类型的实例时,不能向超类型的构造函数中传递参数

# 2.2、借用构造函数

  • 其基本思想为:在子类型的构造函数中调用超类型构造函数。

    function SuperType(name) {
      this.name = name;
    }
    function SubType(name) {
      SuperType.call(this, name);
    }
  • 优点

    1. 可以向超类传递参数
    2. 解决了原型中包含引用类型值被所有实例共享的问题
  • 缺点

    1. 方法都在构造函数中定义,函数复用无从谈起
    2. 另外超类型原型中定义的方法对于子类型而言都是不可见的

# 2.3、组合继承

  • 组合继承指的是将原型链和借用构造函数技术组合到一块,从而发挥二者之长的一种继承模式。基本思路:使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承,既通过在原型上定义方法来实现了函数复用,又保证了每个实例都有自己的属性。

    function SuperType() {
      this.name = "zc";
      this.colors = ["pink", "blue", "green"];
    }
    function SubType() {
      SuperType.call(this);
    }
    SubType.prototype = new SuperType();
    SubType.prototype.constructor = SubType;
    let a = new SubType();
    let b = new SubType();
    
    a.colors.push("red");
    console.log(a.colors); //[ 'pink', 'blue', 'green', 'red' ]
    console.log(b.colors); //[ 'pink', 'blue', 'green' ]
  • 优点

    1. 可以向超类传递参数
    2. 每个实例都有自己的属性
    3. 实现了函数复用
  • 缺点

    1. 无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。

# 2.4、原型式继承

  • 原型式继承继承的基本思想:在 object () 函数内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例,从本质上讲,object () 对传入的对象执行了一次浅拷贝。

    ECMAScript5 通过新增 Object.create () 方法规范了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象 (可以覆盖原型对象上的同名属性),在传入一个参数的情况下,Object.create () 和 object () 方法的行为相同。

    function object(o) {
      function F() {}
      F.prototype = o;
      return new F();
    }
  • 缺点

    1. 同原型链实现继承一样,包含引用类型值的属性会被所有实例共享

# 2.5、寄生式继承

  • 寄生式继承是与原型式继承紧密相关的一种思路。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部已某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。

    function object(o) {
      function F() {}
      F.prototype = o;
      return new F();
    }
    function createAnother(original) {
      var clone = object(original); //通过调用函数创建一个新对象
      clone.sayHi = function () {
        //以某种方式增强这个对象
        console.log("hi");
      };
      return clone; //返回这个对象
    }
  • 优点

    1. 基于 person 返回了一个新对象 -—— person2,新对象不仅具有 person 的所有属性和方法,而且还有自己的 sayHi () 方法。在考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。
  • 缺点

    1. 使用寄生式继承来为对象添加函数,会由于不能做到函数复用而效率低下。
    2. 同原型链实现继承一样,包含引用类型值的属性会被所有实例共享。

# 2.6、寄生组合式继承

  • 所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法,基本思路:

    不必为了指定子类型的原型而调用超类型的构造函数,我们需要的仅是超类型原型的一个副本,本质上就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

    function inheritPrototype(subType, superType) {
      var prototype = object(superType.prototype); //创建对象
      prototype.constructor = subType; //增强对象
      subType.prototype = prototype; //指定对象
    }
    function SuperType(name) {
      this.name = name;
      this.colors = ["pink", "blue", "green"];
    }
    function SuberType(name, age) {
      SuperType.call(this, name);
      this.age = age;
    }
    inheritPrototype(SuberType, SuperType);
  • 步骤

    第一步:创建超类型原型的一个副本

    第二步:为创建的副本添加 constructor 属性

    第三步:将新创建的对象赋值给子类型的原型

  • 优点

    1. 只调用了一次超类构造函数,效率更高。避免在 SuberType.prototype 上面创建不必要的、多余的属性,与其同时,原型链还能保持不变。因此寄生组合继承是引用类型最理性的继承范式。

# 2.7、ES6 继承

  • Class 可以通过 extends 关键字实现继承

    class SuperType {
      constructor(age) {
        this.age = age;
      }
      getAge() {
        console.log(this.age);
      }
    }
    class SubType extends SuperType {
      constructor(age, name) {
        super(age); // 调用父类的constructor(x, y)
        this.name = name;
      }
      getName() {
        console.log(this.name);
      }
    }
  • 对于 ES6 的 class 需要做以下几点说明

    1. class 声明会提升,但不会初始化赋值。Foo 进入暂时性死区,类似于 let、const 声明变量。
    2. class 声明内部会启用严格模式。
    3. class 的所有方法(包括静态方法和实例方法)都是不可枚举的。
    4. class 的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有 [[construct]],不能使用 new 来调用。
    5. 必须使用 new 调用 class
    6. class 内部无法重写类名

# 使用 extends 关键字实现继承,有几点需要特别说明

  • 子类必须在 constructor 中调用 super 方法,否则新建实例时会报错。如果没有子类没有定义 constructor 方法,那么这个方法会被默认添加。在子类的构造函数中,只有调用 super 之后,才能使用 this 关键字,否则报错。这是因为子类实例的构建,基于父类实例,只有 super 方法才能调用父类实例。
  • ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面(Parent.apply (this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到 this 上面(所以必须先调用 super 方法),然后再用子类的构造函数修改 this

# 三、相关题目

# 3.1、写一个原型链继承的例子

  • 详细请观看上文。

# 3.2、描述 new 一个对象的过程

# 3.2.1、思路分析

  1. 创建一个新对象 obj
  2. 把 obj 的 proto 指向 构造函数.prototype 实现继承
  3. 执行构造函数,传递参数,改变 this 指向
  4. 最后把 obj 返回
伪代码:new Person("John") = {
                var obj = {};
	obj.__proto__ = Person.prototype;
	var result = Person.call(obj,"John");
	return typeof result === 'object' ? result : obj; // 如果无返回值或者返回一个非对象值,则将obj返回作为新对象
}

# 3.2.2、优秀的写法

function _new(fn, ...arg) {
  const obj = Object.create(fn.prototype);
  const ret = fn.apply(obj, arg);
  return ret instanceof Object ? ret : obj;
}

【个人倾向于后面一种】

function _new(fn, ...arg) {
  let obj = {};
  obj.__proto__ = fn.prototype;
  let ret = fn.apply(obj, arg);
  return ret instanceof Object ? ret : obj;
}

# 3.2.3、为什么 return ret instanceof Object ? ret : obj; 需要存在这一步骤?

这是因为 new 一个实例的时候,如果没有 return,

就会根据构造函数内部 this 绑定的值生成对象,如果有返回值,

就会根据返回值生成对象,为了模拟这一效果,就需要判断 apply 后是否有返回值。

# 3.2.4、总结 new 的过程中发生了什么

  1. 令 john 的 proto 属性指向 Person.prototype,确立了这条原型链, 导致 john 能通过原型链继承 Person.prototype 中的部分属性,可以简单地视 john 和 Person.prototype 是继承关系。

  2. john 是 Person 构造函数 的实例 john instanceof Person; //true

  3. 我们再来了解一下 instanceof 的内部原理,以应证我们的图是正确的

    var L = A.__proto__;
    var R = B.prototype;
    if (L === R) return true;

单线程和异步

# 专题总结:单线程和异步

拿到 字节跳动实习生 offer 总结

回馈分享一波自己的知识点总结

希望读者依此构建自己的知识树(思维导图)

偷懒一下:可参考我自己总结思维导图 : 点这里

附带:高频面试题积累文档。 来自于(学长、牛客网等平台)

自己开发的博客地址:zxinc520.com

github 地址: 点击

此篇 js - 【单线程和异步】 知识点: 全部弄懂了,面试很容易。

# 一、单线程和异步

# 1.1、同步 vs 异步

  • 同步是什么?
    • 简单来说:一定要等任务执行完了,得到结果,才执行下一个任务。
    • 指某段程序执行时会阻塞其它程序执行,其表现形式为程序的执行顺序依赖程序本身的书写顺序
  • 异步是什么?
    • 指某段程序执行时不会阻塞其它程序执行,其表现形式为程序的执行顺序不依赖程序本身的书写顺序
    • 实现方式:event loop【事件轮询】

# 1.2、异步和单线程

  • 单线程

    • 是什么?单线程就是同时只做一件事,两段 JS 不能同时 执行
    • 为什么是单线程?
      • 避免 DOM 渲染的冲突
        1. 浏览器需要渲染 DOM
        2. JS 可以修改 DOM 结构
        3. JS 执行的时候,浏览器 DOM 渲染会暂停
        4. 两段 JS 也不能同时执行(都修改 DOM 就冲突了)
        5. webworker 支持多线程,但是不能访问 DOM
  • 单线程的解决方案 ?

    • 异步
      • 异步暴露出的问题
        1. 没按照书写方式执行,可读性差
        2. callback 中不容易模块化
  • event loop

    • 是什么?
    • 事件轮询, JS 实现异步 的具体解决方案
    • 具体
      • 同步代码,直接执行
      • 异步函数先放在 异步队列 中
      • 待同步函数执行完毕,轮询执行 异步队列 的函数

# 1.3、宏队列和微队列

macrotask (宏任务) 和 microtask (微任务)

面试常考题【promise 回调函数和定时器任务的顺序问题】

  • 宏任务:

    script(整体代码)
    setTimeout
    setInterval
    I/O
    UI交互事件
    postMessage
    MessageChannel
    setImmediate(Node.js 环境)
  • 微任务

    Promise.then
    Object.observe
    MutaionObserver
    process.nextTick(Node.js 环境)

执行机制:

  1. 执行一个宏任务(栈中没有就从事件队列中获取)
  2. 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  3. 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  4. 当前宏任务执行完毕,开始检查渲染,然后 GUI 线程接管渲染
  5. 渲染完毕后,JS 引擎线程继续,开始下一个宏任务(从宏任务队列中获取)

# 经典面试题

console.log("script start");
let promise1 = new Promise(function (resolve) {
  console.log("promise1");
  resolve();
  console.log("promise1 end");
}).then(function () {
  console.log("promise2");
});
setTimeout(function () {
  console.log("settimeout");
});
console.log("script end");
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}
async function async2() {
  console.log("async2");
}

console.log("script start");
async1();
console.log("script end");

// 输出顺序:script start->async1 start->async2->script end->async1 end

# 1.4、前端异步的场景

  • 简单来说:所有的 “等待情况” 都需要异步
  • 定时任务:setTimeout,setInterval
  • 网络请求:ajax 请求,动态 <img > 加载
  • 事件绑定

# 1.5、Web Worker

就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。

# 1.6、模块化发展历程

可从 IIFE、AMD、CMD、CommonJS、UMD、webpack (require.ensure)、ES Module、<script type=“module” > 这几个角度考虑。

作用 :模块化主要是用来抽离公共代码,隔离作用域,避免变量冲突等。

  1. IIFE

    • 使用自执行函数来编写模块化

    • 特点:

      在一个单独的函数作用域中执行代码,避免变量冲突。

  2. AMD

    • 使用 requireJS 来编写模块化

    • 特点:依赖必须提前声明好

    • 简单实现

      define("./index.js", function (code) {
        // code 就是index.js 返回的内容
      });
  3. CMD

    • 使用 seaJS 来编写模块化

    • 特点:支持动态引入依赖文件

    • 简单实现

      define(function (require, exports, module) {
        var indexCode = require("./index.js");
      });
  4. CommonJS

    • nodejs 中自带的模块化
    • var fs = require(‘fs’);
  5. UMD

    • 兼容 AMD,CommonJS 模块化语法
  6. webpack(require.ensure)

    • webpack 2.x 版本中的代码分割
  7. ES Modules

    • ES6 引入的模块化,支持 import 来引入另一个 js
    • import a from ‘a’;

# 1.6.1、AMD 与 CMD 的比较

  • 定义

    AMD 和 CMD 都是用于浏览器端的模块规范

  • AMD

    • AMD 是 RequireJS 在推广过程中对模块定义的规范化产出
    • 其主要内容就是定义了 define 函数该如何书写,只要你按照这个规范书写模块和依赖,require.js 就能正确的进行解析。
  • CMD

    • CMD 其实就是 SeaJS 在推广过程中对模块定义的规范化产出
    • 主要内容就是描述该如何定义模块,如何引入模块,如何导出模块,只要你按照这个规范书写代码,sea.js 就能正确的进行解析
  • AMD 与 CMD 的区别

    1. AMD 推崇依赖前置,CMD 推崇依赖就近
    2. AMD 是提前执行,CMD 是延迟执行。

# 1.6.2、CommonJS 与 AMD 的比较

在服务器端比如 node,采用的则是 CommonJS 规范。

AMD 和 CMD 都是用于浏览器端的模块规范

  1. CommonJS 规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。

  2. AMD 规范则是非同步加载模块,允许指定回调函数。

    由于 Node.js 主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以 CommonJS 规范比较适用。

  3. 但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用 AMD 规范。

# 16.3、ES6 与 CommonJS 的比较

注意!浏览器加载 ES6 模块,也使用 <script > 标签,但是要加入 type=“module” 属性。

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

# 1.7、async 和 defer

  • 共同点

    两者都会并行下载,不会影响页面的解析。

  • defer:defer 会按照顺序在 DOMContentLoaded 前按照页面出现顺序依次执行。

  • async :async 则是下载完立即执行

  • 具体解析【剖析】

    • 先来看一个普通的 script 标签。<script src=“a.js”></script >

      • 浏览器会做如下处理:

        1、停止解析 document.

        2、请求 a.js

        3、执行 a.js 中的脚本

        4、继续解析 document

    • <script src="d.js" defer></script>
      <script src="e.js" defer></script>
      <!--code6-->
      不阻止解析 document, 并行下载 b.js, c.js
      当脚本下载完后立即执行。(两者执行顺序不确定,执行阶段不确定,可能在 DOMContentLoaded 事件前或者后 )

# async 和 defer 总结

  • 两者都不会阻止 document 的解析

  • defer 会在 DOMContentLoaded 前依次执行 (可以利用这两点哦!)

  • async 则是下载完立即执行,不一定是在 DOMContentLoaded 前

  • async 因为顺序无关,所以很适合像 Google Analytics 这样的无依赖脚本

# 1.8、异步编程 6 种解决方案

  1. 回调函数(Callback)

    • 回调函数是异步操作最基本的方法

    • ajax(url, () => {

      ​ // 处理逻辑

      })

    • 缺点

      • 容易写出回调地狱(Callback hell)
      • 不能使用 try catch 捕获错误,不能直接 return
  2. 事件监听

    f1.on("done", f2);
  3. 发布订阅

    jQuery.subscribe("done", f2);
  4. Promise

    • 是什么?

      • promise 是目前 JS 异步编程的主流解决方案,遵循 Promises/A+ 方案。Promise 用于异步操作,表示一个还未完成但是预期会完成的操作。
      • Promise 是 ES6 引入的一个新的对象,他的主要作用是用来解决 JS 异步机制里,回调机制产生的 “回调地狱”。它并不是什么突破性的 API,只是封装了异步回调形式,使得异步回调可以写的更加优雅,可读性更高,而且可以链式调用。
    • 剖析

      • promise 本身相当于一个状态机,拥有三种状态

        • pending
        • fulfilled
        • rejected

        一个 promise 对象初始化时的状态是 pending,调用了 resolve 后会将 promise 的状态扭转为 fulfilled,调用 reject 后会将 promise 的状态扭转为 rejected,这两种扭转一旦发生便不能再扭转该 promise 到其他状态。

    • Promise 如何使用

      构造一个 promise 对象,并将要执行的异步函数传入到 promise 的参数中执行,并且在异步执行结束后调用 resolve ( ) 函数,就可以在 promise 的 then 方法中获取到异步函数的执行结果

    • Promise 原型上的方法

      1. Promise.prototype.then(onFulfilled, onRejected)
      2. Promise.prototype.catch(onRejected)
      3. Promise.prototype.finally(onFinally)
    • Promise 静态方法

      1. Promise.all()

        Promise.all 接收一个 promise 对象数组作为参数,只有全部的 promise 都已经变为 fulfilled 状态后才会继续后面的处理

      2. Promise.race()

        这个函数会在 promises 中第一个 promise 的状态扭转后就开始后面的处理(fulfilled、rejected 均可)

      3. Promise.resolve()

      4. Promise.reject()

    • 优点

      将异步操作以同步操作的流程表达出来,promise 链式调用,更好地解决了层层嵌套的回调地狱

    • 缺点

      1. 不能取消执行。
      2. 无法获取当前执行的进度信息(比如,要在用户界面展示进度条)。
      3. 外部无法捕捉 Promise 内部抛出的错误
  5. generator 函数

    • 是什么

      • Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
      • 如果说 JavaScript 是 ECMAScript 标准的一种具体实现、Iterator 遍历器是 Iterator 的具体实现,那么 Generator 函数可以说是 Iterator 接口的具体实现方式。
      • Generator 函数可以通过配合 Thunk 函数更轻松更优雅的实现异步编程和控制流管理
    • 描述

      • 执行 Generator 函数会返回一个遍历器对象,每一次 Generator 函数里面的 yield 都相当一次遍历器对象的 next () 方法,并且可以通过 next (value) 方法传入自定义的 value, 来改变 Generator 函数的行为。
    • 能封装异步任务的根本原因

      • 最大特点就是可以交出函数的执行权(即暂停执行)。Generator 函数可以暂停执行和恢复执行
    • 两个特征

      • function 关键字与函数名之间有一个星号
      • 函数体内部使用 yield 表达式,定义不同的内部状态(yield 在英语里的意思就是 “产出”)。
    • 过程

      Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)

    • Generator 及其异步方面的应用

      • Generator 函数将 JavaScript 异步编程带入了一个全新的阶段
    • 总结

      调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的 next 方法,就会返回一个有着 value 和 done 两个属性的对象。value 属性表示当前的内部状态的值,是 yield 表达式后面那个表达式的值;done 属性是一个布尔值,表示是否遍历结束。

    • demo

      var fetch = require("node-fetch");
      function* gen() {
        var url = "https://api.github.com/users/github";
        var result = yield fetch(url);
        console.log(result.bio);
      }
  6. async 和 await

    • 含义

      ES2017 标准引入了 async 函数,使得异步操作变得更加方便。

    • 是什么?

      • 一句话,它就是 Generator 函数的语法糖。
      • 一比较就会发现,async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await,仅此而已。
      • async 函数可以理解为内置自动执行器的 Generator 函数语法糖,它配合 ES6 的 Promise 近乎完美的实现了异步编程解决方案。
    • 相对于 Promise,优势体现在

      1. 处理 then 的调用链,能够更清晰准确的写出代码
      2. 并且也能优雅地解决回调地狱问题
    • 相对 Generator 函数,体现在以下 4 点

      1. 内置执行器。 Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行
      2. 更好的语义。 async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果
      3. 更广的适用性。 co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)
      4. 返回值是 Promise。async 函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用 then 方法指定下一步的操作。
    • 缺点

      当然 async/await 函数也存在一些缺点,因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低,代码没有依赖性的话,完全可以使用 Promise.all 的方式。

# 总结

  1. JS 异步编程进化史:callback -> promise -> generator -> async + await
  2. async/await 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里
  3. async/await 可以说是异步终极解决方案了

# 二、相关面试问题

  1. 什么是单线程,和异步有什么关系?

    • 单线程就是同时只做一件事,两段 JS 不能同时 执行
    • 原因就是 为了避免 DOM 渲染的冲突
    • 异步是一种 “无奈” 的解决方案,虽然有很多问题
  2. 是否用过 jQuery 的 Deferred

    • 步骤

      可以 jQuery 1.5 对 ajax 的改变举例

      说明如何简单的封装,使用 Deferred

      说明 ES6 promise 和 Deferred 的区别

    • jQuery 1.5 的变化

      • 无法改变 JS 异步和单线程的本质

      • 只能从写法上杜绝 callback 这种形式

      • 它是一种语法糖形式,但是解耦了代码

      • 很好的体现:开放封闭原则

      • ajax 为例

        var ajax = $.ajax("data.json");
        ajax
          .done(function () {
            console.log("success 1");
          })
          .fail(function () {
            console.log("error");
          })
          .done(function () {
            console.log("success 2");
          });
        
        console.log(ajax); //返回一个 deferred 对象
    • 使用 jQuery Deferred

      function waitHandle() {
        var dtd = $.Deferred(); //创建一个 Deferred 对象
        var wait = function (dtd) {
          //要求传入一个 Deferred 对象
          var task = function () {
            console.log("执行完成");
            dtd.resolve(); //表示异步任务已经完成
            // dtd.reject()  //表示异步任务失败或出错
          };
          setTimeout(task, 2000);
          return dtd; // 要求返回 Deferred 对象
        };
        // 注意,这里一定要有返回值
        return wait(dtd);
      }

html 面试考点全面总结下篇

# html 面试考点全面总结下篇

拿到 字节跳动实习生 offer 总结

回馈分享一波自己的知识点总结

希望读者依此构建自己的知识树(思维导图)

偷懒一下:可参考我自己总结思维导图 : 点这里

附带:高频面试题积累文档。 来自于(学长、牛客网等平台)

自己开发的博客地址:zxinc520.com

github 地址: 点击

此篇 html 共总结 22 大知识点: 全部弄懂了,面试很容易。

# 11、label 标签

作用:用于定义表单控件的关系,点击时自动将焦点移至相关联的控件。

# 两个有用属性

  • for
    • 关联相关控件
    • 通过控件 id 关联
  • accessKey :设置访问快捷键 例如:accesskey=“h”

注意 :该标签不能为 a 和 button 标签的后代

link :建议使用

@import :慎用 【会造成 “无样式内容闪烁”】

  1. 从属和作用
    • link 是 HTML 提供的标签
      • 可以加载 css
      • 可以定义 rel 等属性(rel 属性规定当前文档与被链接文档之间的关系。) 【技巧:这里引申到 预加载知识:可以关注 Resource Hint 标准 — 页面加载性能利器
    • @import 是 css 提供的语法
      • 只有导入样式表的作用
  2. 加载顺序
    • link 在页面加载时 css 同时被加载
    • @import 引入的 css 需要等页面加载后再加载
  3. 兼容性问题
    • link 是 HTML 提供的语法,没有兼容性问题
    • @import 是 css2.1 提供的语法,ie5 以上才兼容
  4. DOM 可控性
    • js 可以通过插入 link 标签来改变样式
    • js 不可以通过 @import 去引入新的 css 文件来改变样式

# 13、target

属性作用:指定所连接的页面在浏览器窗口中的打开方式

# 属性
  • _self(默认值):在当前窗口打开
  • _blank: 在新窗口中打开
  • _parent : 在父级窗口打开
  • _top : 在顶级窗口打开

# 14、部分标签 / 属性区别

# 标签区别

  • title 和 h1
    • title :只表示是个标题
    • h1-h7
      • 表示层次明确的标题
      • 对页面信息的抓取有帮助
  • b 与 strong
    • b:展示为粗体
    • strong
      • 标明重点内容,有语气加强的含义 u
      • 使用阅读设备时,会重读
  • i 与 em
    • i:展示为斜体
    • em:表示强调的文本

# 属性区别

  • src 与 href
    • src:引入;将指定资源应用到文档内.
    • href:引用;建立与当前文档之间的链接.
  • 【img】title 和 alt
    • title
      • 全局属性
      • 提供关于元素的额外信息
      • 鼠标移至显示
    • alt
      • 用于图片无法加载时显示
      • web Quality(无障碍)易访问的

# 15、Shadow DOM(影子 DOM)

# 是什么?

浏览器的一种能力 :渲染时插入独立的 DOM 树

# 特点?

  • 与原始 DOM 完全隔离
  • 具有自己的元素和样式

# 作用?

  • 封装需要隔离外部的文档细节 / 组件
  • 防止开发人员随意修改样式

# 使用方式?

  • Node1.attachShadow(Node2)
  • Node2.innerHTML = …

# 16、浏览器的数据存储方式有哪些

分为三类来讲:

cookie 、localStorage 和 sessionStorage 、userData。

h5 之前,存储主要用 cookies,缺点是在请求头上带着数据,导致流量增加。大小限制 4k

# 创建目的
  • 为了保持 HTTP 的状态
  • 为了识别用户信息而储存在本地上的数据
# 特点
  1. 可储存大小为 4k
  2. 储存个数有限制(各浏览器不同)
  3. 有效时间在设置的 cookie 过期时间之前一直有效

# localStorage 和 sessionStorage

创建目的:便于客户端储存数据

# 相同点
  • 都由 HTML5 Web Storage API 提供
  • 在本地保存
  • 可储存大小 5M 以上
# 不同点
  • 有效时间不同
    • localStorage(以键值对 (Key-Value) 的方式存储)
      • 储存持久数据
      • 浏览器关闭后数据不丢失除非主动清除数据
    • sessionStorage
      • 数据在当前浏览器关闭后自动删除
  • 作用域不同
    • localStorage 在所有同源窗口中都是共享的;cookie 也是在所有同源窗口中都是共享的。
    • sessionStorage 不在不同的浏览器页面中共享,即使是同一个页面

安全性:需要注意的是,不是什么数据都适合放在 Cookie、localStorage 和 sessionStorage 中的,因为它们保存在本地容易被篡改,使用它们的时候,需要时刻注意是否有代码存在 XSS 注入的风险。所以千万不要用它们存储你系统中的敏感数据。

# userData

# 特点
  • IE 专属 :早期 IE 浏览器用来本地储存数据用的
  • 以文件的形式保存在磁盘上 :持久化储存方式
  • 可以设置失效日期
  • 可储存大小 1MB 左右

注意:使用 IE 条件注释来避免其它浏览器载入上述代码 <!–[if IE]><[end If]– >

# 17、如何实现标签页面的通信

  1. 方法一 :使用 localStorage

    • 使用 localStorage.setItem (key,value) 添加内容

    • 使用 Storage 事件监听添加、修改、删除的动作

      window.onstorage = (e) => {console.log(e)}
      // 或者这样
      window.addEventListener('storage', (e) => console.log(e)
  2. 方法二:使用 cookie+setInterval

    • 将要传递的信息储存在 cookie 中
    • 每隔一定时间读取 cookie 信息,获取要传递的信息
    • 具体描述 :1、在页面 A 设置一个使用 setInterval 定时器不断刷新,检查 Cookies 的值是否发生变化,如果变化就进行刷新的操作。 2、由于 Cookies 是在同域可读的,所以在页面 B 审核的时候改变 Cookies 的值,页面 A 自然是可以拿到的。这样做确实可以实现我想要的功能,但是这样的方法相当浪费资源。虽然在这个性能过盛的时代,浪费不浪费也感觉不出来,但是这种实现方案,确实不够优雅。
  3. 方法三 :websocket 通讯(HTML5)

    • 定义:WebSocket 是 HTML5 新增的协议,它的目的是在浏览器和服务器之间建立一个不受限的双向通信的通道,比如说,服务器可以在任意时刻发送消息给浏览器。
    • WebSocket 连接必须由浏览器发起,特点
      • 建立在 TCP 协议之上,服务器端的实现比较容易。
      • 与 HTTP 协议有着良好的兼容性。默认端口也是 80 和 443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
      • 数据格式比较轻量,性能开销小,通信高效。
      • 可以发送文本,也可以发送二进制数据。
      • 没有同源限制,客户端可以与任意服务器通信。
      • 协议标识符是 ws(如果加密,则为 wss),服务器网址就是
  4. SharedWorker(html5 浏览器的新特性 SharedWorker)

    • 本质还是单线程,只是利用了浏览器不同 JS 引擎
    • 必须在服务器上才跑得动
    • IE 未兼容

共同点:记录用户状态

# 区别:

  • 什么是 Cookie?

    HTTP Cookie(也叫 Web Cookie 或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。

  • 采用的是在客户端保持状态的方案 : 即运行在客户端

  • 有大小限制,存储个数有限

  • 有安全隐患 :通过某些手法可以篡改本地储存的信息来欺骗客户端

  • 支持跨域名访问

# session
  • 什么是 Session?

    Session 代表着服务器和客户端一次会话的过程。Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当客户端关闭会话,或者 Session 超时失效时会话结束。

  • 采用的是在服务端保持状态的方案: 即运行在服务端

  • 没有大小限制和服务器内存大小有关

  • 过多会增加服务器压力

  • 仅在他所在的域名内有效

1、用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建创建对应的 Session ,请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器,浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名。

2、当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。

3、根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。

  • 第一种方案,每次请求中都携带一个 SessionID 的参数,也可以 Post 的方式提交,也可以在请求的地址后面拼接 xxx?SessionID=123456…。

  • 第二种方案,Token 机制。Token 机制多用于 App 客户端和服务器交互的模式,也可以用于 Web 端做用户状态管理。

    Token 的意思是 “令牌”,是服务端生成的一串字符串,作为客户端进行请求的一个标识。Token 机制和 Cookie 和 Session 的使用机制比较类似。

    当用户第一次登录后,服务器根据提交的用户信息生成一个 Token,响应时将 Token 返回给客户端,以后客户端只需带上这个 Token 前来请求数据即可,无需再次登录验证。

# 18.2、如何考虑分布式 Session 问题?

  • 在互联网公司为了可以支撑更大的流量,后端往往需要多台服务器共同来支撑前端用户请求,那如果用户在 A 服务器登录了,第二次请求跑到服务 B 就会出现登录失效问题。
  • 分布式 Session 一般会有以下几种解决方案
    • Nginx ip_hash 策略,服务端使用 Nginx 代理,每个请求按访问 IP 的 hash 分配,这样来自同一 IP 固定访问一个后台服务器,避免了在服务器 A 创建 Session,第二次分发到服务器 B 的现象。
    • Session 复制,任何一个服务器上的 Session 发生改变(增删改),该节点会把这个 Session 的所有内容序列化,然后广播给所有其它节点。
    • 共享 Session,服务端无状态话,将用户的 Session 等信息使用缓存中间件来统一管理,保障分发到每一个服务器的响应结果都一致。
  • 极高的扩展性和可用性
  • 不需要使用大量服务器资源
  • 简单性 Cookie 是一种基于文本的轻量结构,包含简单的键值对,结构简单。
# session
  • 易于读写
  • 易于站点的用户化

# 18.4:cookie 和 session 常见攻击方式及解决方案

  • 具体
    • 直接访问 Cookie 文件查找想要的机密文件
    • 进行 Cookie 信息传递时被截取
    • 攻击者伪造 Cookie 信息,客户端获取后进行操作
  • 解决方案
    • 不要早 Cookie 中保存敏感信息
    • 不要早 Cookie 中保存没有经过加密的或者容易被解密的敏感信息
    • 对从客户端获取得的 Cookie 信息进行严格校验
# session
  • 具体
    • 会话劫持(通过获取用户 Session ID 后,使用该 Session ID 登录目标账号)
    • 会话固定(诱骗受害者使用攻击者指定的会话标识 Session ID 的攻击手段)
  • 解决方案
    • 使用 User-Agent 检测请求的一致性,设置 HttpOnly,可以防止客户端脚本访问这个 Cookie,从而有效的防止 XSS 攻击;关闭透明化 Session ID;更改 Session 名称
    • 用户登录时生成新的 Session ID

# 19、谈谈对 WebSocket 的认识

创建原因:HTTP 协议只能由客户端发起 单向连接

# 19.1、是什么?

  • HTML5 中的协议,支持持久连接
  • WebSocket 是基于 HTTP 协议的 : 借用了 Http 协议来完成一部分握手
  • 是真正意义上的双向绑定

# 19.2、WebSocket 区别 http 协议

  • http 协议 不支持持久性连接
  • HTTP1.1 中出现 keep-alive,合并多个 http 请求
  • HTTP 的生命周期通过 Request 来界定 : 一个 Request 对应一个 Response
  • Response 是被动的,不能主动发起

# 19.3、如何模拟双向通信

  • 短轮询
    • 客户端定时向服务器发送 Ajax 请求,服务器接到请求后马上返回响应信息并关闭连接。
    • 优点 : 后端编写容易
    • 缺点 : 请求中大半是无用,浪费宽带和服务器资源
    • 适用 : 小型应用
  • 长轮询
    • 客户端向服务器发送 Ajax 请求,服务器接到请求后 hold 住连接,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求。
    • 优点 :在无消息的情况下不会频繁的请求,耗费资源小
    • 缺点
      • 服务器 hold 连接会消耗资源
      • 返回数据顺序无保证,难于管理维护
  • 长连接
    • 在页面嵌入一个隐藏 iframe,将这个隐藏 iframe 的 src 属性设为对一个长连接的请求或是采用 xhr 请求,服务器端就能源源不断的往客户端输入数据
    • 优点
      • 消息及时到达,不发无用请求
      • 管理起来也相对方便
    • 缺点:服务器维护一个长连接会增加开销

# 20、渲染 | 了解网页渲染流程与优化技巧

# 20.1、生成网页步骤

  • HTML 代码转成 DOM(Document Object Model): 解析 HTML 生成
  • CSS 代码转化成 CSSOM(CSS Object Model): 解析 CSS 生成
  • 结合 DOM 和 CSSOM 生成一颗渲染树: 包含每个节点的视觉信息
  • 生成布局(layout): 将所有渲染树的所有节点进行平面合成
  • 将布局绘制(paint)在屏幕上

# 20.2、重新渲染 (重排【回流】和重绘)

# 重排(回流)

重新生成布局

# 布局改了就一定会重排
  1. 添加或删除可见的 DOM 元素
  2. 元素位置改变
  3. 元素尺寸改变 —— 边距、填充、边框、宽度和高度
  4. 内容改变 —— 比如文本改变或者图片大小改变而引起的计算值宽度和高度改变;
  5. 页面渲染器初始化
  6. 浏览器窗口尺寸改变 ——resize 事件发生时;

重排一定需要重绘

var s = document.body.style;
s.padding = "2px"; // 回流+重绘
s.border = "1px solid red"; // 再一次 回流+重绘
s.color = "blue"; // 重绘
s.backgroundColor = "#ccc"; // 重绘
s.fontSize = "14px"; // 再一次 回流+重绘
document.body.appendChild(document.createTextNode('abc!'));// 添加node,再一次 回流+重绘

# 重绘

只是影响元素的外观,风格,而不会影响布局的,比如 background-color。则就叫称为重绘。

# 重新渲染出现情况

  • 修改 DOM
  • 修改样式表
  • 用户事件
    • 鼠标悬停
    • 页面滚动
    • 输入框输入文字
    • 改变窗口等

# 21、性能优化技巧

回答性能优化问题时:分两层阐述:

1、底层:重排【回流】和重绘层级 — 下面针对此层

2、应用层(雅虎军规 35 条)— 在性能优化专题会仔细分析。

# 21.1、减少重新渲染频率

  1. DOM 的多个读 / 写操作应该放在一起。

    • 不要两个读操作之间,加入一个写操作
  2. 如果某个样式是通过重排得到的,那么最好缓存结果。避免下一次用到的时候又要重排

  3. 不要一条条地改变样式

    • 通过改变 class,csstext,一次改变样式
  4. 尽量使用离线 DOM,而不是真实的网页 DOM,来改变元素样式

    • 例如

      • 操作 Document Fragment 对象

      • cloneNode 克隆节点操作后替换

  5. 使用虚拟 DOM 的脚本库,比如 React,vue 等

  6. 很新颖(嘻嘻)

  • 设为 dispaly:none(需要一次重排和重绘)后进行 n 次操作,最后再恢复显示(需要一次重排和重绘)
  • 用两次重新渲染,取代了可能 n(可能是个大树)次的重新渲染
  • 只有在必要的时候,才将元素的 display 属性为可见
  • visibility:hidden 的元素只对重绘有影响,不影响重排
  • 隐藏后不可见的元素读写不影响重排和重绘
  1. 使用调整重新渲染的方法 : 可以大幅度提高网页性能

    • 使用 window.requestAnimationFrame () 方法
      • 作用:将某些代码放到下一次重新渲染时执行
      • 适用
        • 页面滚动事件的监听函数
        • 网页动画
    • 使用 window.requestdleCallback () 方法
      • 暂时就 Chrome 支持

# 21.2、减少渲染成本

  • position 属性为 absolute 或 fixed 的元素,重排的开销会比较小

    因为它们脱离文档,不用考虑他们对其他元素的影响,所以用 absolute,而少用 float

# 22、HTML5 概括

HTML(超文本标记语言 HyperText Markup Language)的最新本版本

# 新增特性

  1. 语义化元素 推荐使用

  2. 新的通讯方式

    • WebSockets
    • 重新绘制界面 布局没改,样式改了,需要重绘,不一定重排
  3. 缓存

    • 应用程序缓存
      • 使用方式
        • <html manifest=“demo.appcache” >
        • manifest 文件的建议的文件扩展名是:".appcache"
      • 优点
        • 离线浏览 - 用户可在应用离线时使用它们
        • 更快速度 - 已缓存资源加载得更快
        • 减少服务器负载 - 浏览器将只从服务器下载更新过或更改过的资源。
    • 浏览器缓存
      • Web Storage
        • localStorage
        • sessionStorage
  4. 多媒体

    • audio
    • video
  5. 3D & 图像

    • canvas
    • WebGL:用 canvas 元素中的 API 实现 3D 图像功能
    • svg :基于 XML 直接嵌入到 HTML 中的矢量图形格式
  6. 性能 & 集成

    • Web workers

      Web Worker 是为了解决 JavaScript 在浏览器环境中没有多线程的问题。正常形况下,浏览器执行某段程序的时候会阻塞直到运行结束后在恢复到正常状态,而 HTML5 的 Web Worker 就是为了解决这个问题,提升程序的执行效率。 所以 Web Worker 的最佳使用场景是执行一些开销较大的数据处理或计算任务。

    • web worker 的创建

      • worker 是一个对象,通过构造函数 Worker 创建,参数就是一个 js 文件的路径;文件中的 js 代码将运行在主线程之外的 worker 线程;
      • 例如:var myWorker = new Worker (‘worker.js’);
    • History API :允许对浏览器历史记录进行操作

    • XMLHttpRequest Level 2 ((XHR)对象可以与服务器交互。)

      新版本功能:

      • 可以设置 HTTP 请求的时限
      • 可以使用 FormData 对象管理表单数据。
      • 可以上传文件。
      • 可以请求不同域名下的数据(跨域请求)
      • 可以获取服务器端的二进制数据
      • 可以获得数据传输的进度信息

      老版本的缺点:

      • 只支持文本数据的传送,无法用来读取和上传二进制文件。
      • 传送和接收数据时,没有进度信息,只能提示有没有完成。
      • 受到 "同域限制"(Same Origin Policy),只能向同一域名的服务器请求数据。
    • contentEditable

      • 让元素的区域可编辑
      • 已标准化
    • requestAnimationFrame : 允许控制动画渲染以获得更优性能

    • 拖放 API draggable 属性、拖放事件 (dragstart、drag、dragenter、dragleave、dragover、drap、dragend)、dataTransfer 对象

    • 全屏 API

    • 在线和离线事件

  7. 设备访问

    • carnera :能够操作计算机的摄像头
    • 地理位置定位 Geolocation
    • 触控事件
    • 检测设备方向 :横向还是竖向

html 面试考点全面总结上篇

# html 面试考点全面总结上篇

拿到 字节跳动实习生 offer 总结

回馈分享一波自己的知识点总结

希望读者依此构建自己的知识树(思维导图)

偷懒一下:可参考我自己总结思维导图 : 点这里

附带:高频面试题积累文档。 来自于(学长、牛客网等平台)

自己开发的博客地址:zxinc520.com

github 地址: 点击

此篇 html 共总结 22 大知识点: 全部弄懂了,面试很容易。

# 1、浏览器页面由哪三层构成

  • 结构层
    • HTML
    • 构建文件结构
  • 表示层
    • css
    • 设置文档呈现效果
  • 行为层
    • JS 和 DOM 脚本
    • 实现文档的行为

# 2、语义化 | 谈谈 html5 语义化

# 2.1、什么是语义化?

HTML5 的语义化指的是合理使用语义化的标签来创建页面结构,如 header,footer,nav,从标签上即可以直观的知道这个标签的作用,而不是滥用 div。

# 2.2、语义化的优点有

  1. 代码结构清晰,易于阅读,利于开发和维护
  2. 提高用户体验,在样式加载失败时,页面结构清晰
  3. 方便其他设备解析(如屏幕阅读器)根据语义渲染网页。
  4. 有利于搜索引擎优化(SEO),搜索引擎爬虫会根据不同的标签来赋予不同的权重

# 2.3、常用语义化标签有哪些

article | aside | nav | section | header | footer

# 3、HTML5 元素分类

  1. 结构性元素
    • section:在 web 页面应用中,该元素也可以用于区域章节表述;
    • header:页面主题上的头部,注意区别于 head 元素;
    • footer:页面的底部(页脚);
    • nav:是专门用于菜单导航、链接导航的元素,是 navigator 的缩写;
    • article:用于表示一篇文章的主题部分,一般为文字集中显示的区域;
  2. 级块性元素
    • aside:用以表达注记、贴士、侧栏、摘要、插入的引用等作为补充主体的内容;
    • figure:是对多个元素进行组合并展示的元素,通常与 figcaption 联合使用;
    • code:表示一段代码块;
    • dialog:用于表达人与人之间的对话,该元素还包括 dt 和 dd 这两个组合元素,他们常常同时使用。dt 用于表示说话者,而 dd 用来表示说话者的内容。
  3. 行内语义性元素
    • meter:表示特定范围内的数值,可用于工资、数量、百分比等;
    • time:表示时间值;
    • progress:用来表示进度条,可通过对其 max、min、step 等属性进行控制,完成对进度的表示和监视;
    • video:视频元素,用于支持和实现视频(含视频流)文件的直接播放,支持缓冲预载和多种视频媒体格式;
    • audio:音频元素,用于支持和实现音频(音频流)文件的直接播放,支持缓冲预载和多种音频媒体格式;
  4. 交互性元素
    • details:用来表示一段具体的内容,但是内容默认可能不显示,通过某种手段(如单击)与 legend 交互才会显示出来;
    • datagrid:用来控制客户端数据与显示,可以由动态脚本及时更新;
    • menu:主要用于交互菜单;
    • command:用来处理命令按钮。

# 4、常见空元素

含义 :没有元素内容标记的内容【也称自闭合元素】

常用的空元素:



# 5、表单增强 | 新增的 input 类型及属性

# 5.1、新类型

  • color :用于指定颜色的控件
  • number:用于输入浮点数的控件
  • tel:用于输入电话号码的控件;换行会被自动从输入的值中移除 A,但不会执行其他语法。可以使用属性,比如 pattern 和 maxlength 来约束控件输入的值。恰当的时候,可以应用 :valid 和 :invalid CSS 伪类。
  • email:用于编辑 e-mail 的字段。 合适的时候可以使用 :valid 和 :invalid CSS 伪类。
  • url :用于编辑 URL 的字段
  • range :用于输入不精确值控件
  • search :用于输入搜索字符串的单行文本字段。换行会被从输入的值中自动移除。
  • 与时间相关
    • date : 用于输入日期的控件(年,月,日,不包括时间)
    • time : 用于输入不含时区的时间控件
    • datatime 【已弃用】 : 用于输入日期和时间的控件(小时,分钟, 秒,基于 UTC 时区的一小部分。 此功能已从 WHATWG HTML 中删除。
    • datetime-local : 用于输入日期时间控件,不包含时区
    • month : 用于输入年月的控件,不带时区
    • week : 用于输入一个由星期 - 年组成的日期,日期不包括时

# 5.2、新属性

  • placeholder

  • required 必填项

  • list 属性规定输入域的 datalist。datalist 是输入域的选项列表

  • pattern 定义正则

  • autofocus 属性规定在页面加载时,域自动地获得焦点。

  • readonly 该字段只读,不能修改

  • autocomplete 属性规定 form 或 input 域应该拥有自动完成功能。

  • min/max / step

    • min、max 和 step 属性用于为包含数字或日期的 input 类型规定限定(约束)。

      max 属性规定输入域所允许的最大值。

      min 属性规定输入域所允许的最小值。

      step 属性为输入域规定合法的数字间隔(如果 step=“3”,则合法的数是 -3,0,3,6 等)。

# 6、认识 SVG

含义 :可缩放矢量图形(Scalable Vector Graphics,SVG),是一种用于描述基于二维的矢量图形的,基于 XML 的标记语言。

关键词:【基于 XML】【矢量】 【图像格式】

特点

  • 矢量
    • 可以任意缩放
    • 不会破坏图像的清晰度和细节
    • 边缘清晰,适用任何分辨率
  • 文本独立 文字独立于图像
  • 文件小 下载快
  • 颜色控制

# 6.1、SVG 与 HTML5 的 canvas 各有什么优点,哪个更有前途?

Canvas 是使用 JavaScript 程序绘图 (动态生成),SVG 是使用 XML 文档描述来绘图。
从这点来看:SVG 更适合用来做动态交互,而且 SVG 绘图很容易编辑,只需要增加或移除相应的元素就可以了。
同时 SVG 是基于矢量的,所有它能够很好的处理图形大小的改变。Canvas 是基于位图的图像,它不能够改变大小,只能缩放显示;所以说 Canvas 更适合用来实现类似于 Flash 能做的事情 (当然现在 Canvas 与 Flash 相比还有一些不够完善的地方)。
关于最后一点二者谁更有前途:从上面我们可以知道二者是有不同用途的,作为一个开发者,你应该做的是理解应用程序的具体需求并选择正确的技术来实现它。

# 7、浏览器内核

# 7.1、认识内核

  • 渲染引擎 :渲染引擎决定了浏览器如何显示网页的内容以及页面的格式信息
  • JS 引擎
    • 解析和执行 JavaScript 来实现网页的动态效果
    • 引擎越来越独立,内核就倾向于只指渲染引擎

# 7.2、主流浏览器所用的内核

  • IE 浏览器 Trident 内核
  • 谷歌浏览器(chrome)
    • Webkit(之前使用)
    • blink 内核
  • Opera 浏览器
    • blink 内核
    • Presto 内核(之前使用)
      • 渲染速度的优化达到了极致
      • 牺牲了兼容性
  • 火狐浏览器(Firefox)
    • Gecko 内核 : 代码完全公开,可开发程度很高
  • Safari 浏览器 :苹果公司 webkit 内核
  • 国产浏览器
    • 双内核(一个负责兼容,一个负责速度)
    • 常用
      • Trident + webkit
      • Trident + blink

# 8、WEB 标准以及 W3C

# 8.1、web 标准

分为结构、表现、行为

# web 标准是什么?

一系列标准的集合:

  • 结构化标准语言
  • 表现标准语言
  • 行为标准语言
# web 标准诞生原因?

为了解决因浏览器版本不同、软硬件设备不同导致的需多版本开发的问题。

# 8.2、W3C

W3C 对 web 标准提出规范化要求

# 一:结构要求:
  • 遵循的好处
    • 提升搜索引擎对页面的抓取效率
    • 对 SEO 很有帮助
  • 具体
    • 标签字母要小写
    • 标签要闭合
    • 标签不允许随便嵌套
# 二:表现与行为要求:
  • 遵循的好处
    • 使用户浏览者更方便的阅读
    • 使网页开发者之间更好的交流
  • 具体
    • 尽量使用外链 css 样式表和 js 脚本:提高页面渲染速度
    • 页面尽量少用行间样式表 : 使结构和表现分离
    • 标签 id 和 class 等属性名要见文知义

# 9、Doctype(DTD)

作用 : 声明文档的类型风格

告诉浏览器采用何种渲染模式解析页面

# 9.1、渲染模式

  • 怪异模式(兼容模式、混杂模式)
    • 服务于旧式规则
    • 页面以宽松的向后兼容的方式显示,模拟老式浏览器的行为以防止站点无法工作。
  • 标准模式(严格模式)
    • 服务于标准规则
    • 标准模式的排版 和 JS 运作模式都是以该浏览器支持的最高标准运行
  • 近似标准模式
    • 基本是标准模型
    • 有一些是自己的调整

意义:严格模式与混杂模式存在的意义与其来源密切相关,如果说只存在严格模式,那么许多旧网站必然受到影响,如果只存在混杂模式,那么会回到当时浏览器大战时的混乱,每个浏览器都有自己的解析模式。

# 9.2、标准模式和混杂模式的区别?

  • ** 盒模型的处理差异:** 标准 CSS 盒模型的宽度和高度等于内容区的高度和宽度,不包含内边距和边框,而 IE6 之前的浏览器实现的盒模型的宽高计算方式是包含内边距和边框的。因此,对于 IE,怪异模式和标准模式下的盒模型宽高计算方式是不一样的;

  • ** 行内元素的垂直对齐:** 很多早期的浏览器对齐图片至包含它们的盒子的下边框,虽然 CSS 的规范要求它们被对齐至盒内文本的基线。标准模式下,基于 Gecko 的浏览器将会对齐至基线,而在 quirks 模式下它们会对齐至底部。最直接的例子就是图片的显示。在标准模式下,图片并不是与父元素的下边框对齐的,如果仔细观察,你会发现图片与父元素下边框之间存在一点小空隙。那是因为标准模式下,图片是基线对齐的。而怪异模式下,则不存在这个问题。具体请看这篇文章 CSS 深入理解 vertical-align 和 line-height 的基友关系

# 9.3、标准模式和严格模式的区别?

严格模式主要有以下限制:

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用 with 语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量 delete prop,会报错,只能删除属性 delete global [prop]
  • eval 不会在它的外层作用域引入变量
  • eval 和 arguments 不能被重新赋值
  • arguments 不会自动反映函数参数的变化
  • 不能使用 arguments.callee
  • 不能使用 arguments.caller
  • 禁止 this 指向全局对象
  • 不能使用 fn.caller 和 fn.arguments 获取函数调用的堆栈

# 10、meta 标签

<meta > 元素可提供有关页面的元信息(meta-information),比如针对搜索引擎和更新频度的描述和关键词。

# 10.1、四个属性

  • http-equiv 【重要关键词】
    • content-type
      • 定义字符编码
      • 不推荐使用 改用 charset 属性
    • refresh:指定以秒为单位,执行重载和重定向
  • name 【 重要关键词】
    • application-name:应用程序名称
    • keywords : keywords 用来告诉搜索引擎你网页的关键字是什么
    • author : 当前页的作者名
    • viewport
      • 设置浏览器视口
      • 重要关键字
        • width : 视口宽度
        • *-scale (initial-scale): 缩放相关
        • user-scalable : 是否可以手动缩放
        • 例如:<meta name=“viewport” content=“width=device-width, initial-scale=1, maximum-scale=1” >
  • description description 用来告诉搜索引擎你的网站主要内容
  • content :具体描述 、不能单独存在
  • charset(HTML5):推荐使用 utf-8 简化了不同脚本对文件中字符的处理

# 10.2、meta 标签的作用

  1. 搜索引擎优化(SEO)

  2. 定义页面使用语言

  3. 自动刷新并指向新的页面

  4. 实现网页转换时的动态效果

  5. 控制页面缓冲

  6. 网页定级评价

  7. 控制网页显示的窗口

#
#

正则表达式

# 正则表达式

正则表达式是用于匹配字符串中字符组合的模式。在 JavaScript 中,正则表达式也是对象。这些模式被用于 RegExp exec test 方法,以及 String match matchAll replace search split 方法。本章介绍 JavaScript 正则表达式

辅助正则可视化网站:https://regexper.com/

使用正则表达式的方法

方法 描述
exec 一个在字符串中执行查找匹配的 RegExp 方法,它返回一个数组(未匹配到则返回 null)。
test 一个在字符串中测试是否匹配的 RegExp 方法,它返回 true 或 false。
match 一个在字符串中执行查找匹配的 String 方法,它返回一个数组,在未匹配到时会返回 null。
matchAll 一个在字符串中执行查找所有匹配的 String 方法,它返回一个迭代器(iterator)。
search 一个在字符串中测试匹配的 String 方法,它返回匹配到的位置索引,或者在失败时返回 - 1。
replace 一个在字符串中执行查找匹配的 String 方法,并且使用替换字符串替换掉匹配到的子字符串。
split 一个使用正则表达式或者一个固定字符串分隔一个字符串,并将分隔后的子字符串存储到数组中的 String 方法。

# REGEXP 对象

  • JavaScript 通过内置函数对象 RegExp 支持正则表达式
  • 有两种方法实例化 RegExp 对象
    • 字面量
    • 构造函数

# 字面量

var reg = /\bis\b/g;

console.log("he is dog ,he love she how are you is".replace(reg, "IS"));
// he IS dog ,he love she how are you IS

# 构造函数

var reg = new RegExp("\\bis\\b", "g");
console.log("he is dog ,he love she how are you is".replace(reg, "IS"));

// he IS dog ,he love she how are you IS

# 修饰符

在 JavaScript 中,正则表达式标志

  • i

    不区分大小写搜索。

  • g

    全局搜索。

  • m

    多行搜索。

  • u

    使用 unicode 码的模式进行匹配。

  • y

    执行 “粘性” 搜索,匹配从目标字符串的当前位置开始,可以使用 y 标志。

  • s

    允许 . 匹配换行符。

# 元字符

  • 正则表达式由两种基本字符类型组成:
    • 原义文本字符
    • 元字符
  • 元字符是在正则表达式中有特殊含义的非字母字符
元字符 描述
\ 将下一个字符标记为特殊字符或字面值。例如,n 匹配字符 n,而 \n 匹配换行符。序列 \ 匹配 \,而 ( 匹配 (。
^ 匹配输入的开始部分。
$ 匹配输入的结束部分。
* 零次或更多次匹配前面的字符。例如,zo* 匹配 z 或 *zoo*。
+ 一次或更多次匹配前面的字符。例如,zo+ 匹配 zoo,但是不匹配 z
? 零次或一次匹配前面的字符。例如,a?ve? 匹配 never 中的 ve
. 匹配任何单个字符,但换行符除外。
(pattern) 匹配模式并记住匹配项。通过使用以下代码,匹配的子串可以检索自生成的匹配项集合:Item [0]…[n]。要匹配圆括号字符 ( ),请使用 ( 或 )。
x|y 匹配 x 或 y。 例如,z|wood 匹配 zwood。(z|w) oo 匹配 zoowood
n 是一个非负整数。精确匹配 n 次。例如,o {2} 不匹配 Bob 中的 o,但是匹配 foooood 中的前两个 o
在此表达式中,n 是一个非负整数。至少 n 次匹配前面的字符。例如,o {2,} 不匹配 Bob 中的 o,但是匹配 foooood 中的所有 o。o {1,} 表达式等效于 o+,o {0,} 等效于 o*。
m 和 n 变量是非负整数。至少 n 次且至多 m 次匹配前面的字符。例如,o {1,3} 匹配 fooooood 中的前三个 o。o {0,1} 表达式等效于 o?。
[xyz] 一个字符集。匹配任意一个包含的字符。例如,[abc] 匹配 plain 中的 a
[^xyz] 一个否定字符集。匹配任何未包含的字符。例如,[^abc] 匹配 plain 中的 p
[a-z] 字符范围。匹配指定范围中的任何字符。例如,[a-z] 匹配英语字母中的任何小写的字母字符。
[^m-z] 一个否定字符范围。匹配未在指定范围中的任何字符。例如,[m-z] 匹配未在范围 mz 之间的任何字符。
\A 仅匹配字符串的开头。
\b 匹配某个单词边界,即,某个单词和空格之间的位置。例如,er\b 匹配 never 中的 er,但是不匹配 verb 中的 er
\B 匹配非单词边界。ea*r\B 表达式匹配 never early 中的 *ear*。
\d 匹配数字字符。
\D 匹配非数字字符。
\f 匹配换页字符。
\n 匹配换行符。
\r 匹配回车字符。
\s 匹配任何空格,包括空白、制表符、换页字符等等。
\S 匹配任何非空格字符。
\t 匹配跳进字符。
\v 匹配垂直跳进字符。
\w 匹配任何单词字符,包括下划线。此表达式等效于 [A-Za-z0-9_]。
\W 匹配任何非单词字符。此表达式等效于 [^a-za-z0-9__]。
\z 仅匹配字符串的结尾。
\Z 仅匹配字符串的结尾,或者结尾的换行符之前。

字符类:

"a1b2c3d4".replace(/[abc]/g, "X");
//"X1X2X3d4"
"a1b2c3d4".replace(/[^abc]/g, "X");
//"aXbXcXXX"

范围类:

"a1b2c3d4zcczx".replace(/[a-z]/g, "Q");
//"Q1Q2Q3Q4QQQQQ"

"a1b2c3d4zcczxAAAAAAA".replace(/[a-zA-Z]/g, "Q");
//"Q1Q2Q3Q4QQQQQQQQQQQQ"

"2016-09-12".replace(/[0-9]/g, "A");
//"AAAA-AA-AA"

"2016-09-12".replace(/[0-9-]/g, "A");
//"AAAAAAAAAA"

预定义类:

mark

//匹配一个 ab + 数字 + 任意字符 的字符串
ab\d.

边界:

mark

"this is a boy".replace(/\bis\b/g, "IS");
//"this IS a boy"

"@13@12331".replace(/@./g, "Q");
//"Q3Q2331"

"@13@12331".replace(/^@./g, "Q");
//"Q3@12331"

"@13@12331@".replace(/.@$/g, "Q");
//"@13@1233Q"

量词:

mark

贪婪模式:

"12345678".replace(/\d{3,6}/g, "a");
//"a78"

非贪婪模式:

"12345678".replace(/\d{3,6}?/g, "a");
//"aa78"

分组:

"a1b2c3d4".replace(/([a-zA-z]\d){3}/g, "A");
//"Ad4"

或:

"123456789122312".replace(/(123|456)/g, "A");
//"AA789122312"

反向引用:

"2019-10-09".replace(/(\d{4})-(\d{2})-(\d{2})/g, "$3/$2/$1");
//"09/10/2019"

前瞻:

  • 正则表达式从文本头部向尾部开始解析,文本尾部方向,称为 “前”
  • 前瞻 就是正则表达式匹配到规则的时候,向前检查是否符合断言,后顾 / 后瞻 方向相反
  • JavaScript 不支持 后顾
  • 符合和不符合特定断言称为 肯定 / 正向 匹配 和 否定 / 负向 匹配

mark

"a2*3".replace(/\w(?=\d)/g, "A");
//"A2*3"

# 对象属性

  • global:是否全文搜索,默认 false
  • ignore case:是否大小写敏感,默认是 false
  • multiline:多行搜索,默认值是 false
  • lastIndex: 是当前表达式匹配内容的最后一个字符的下一个位置
  • source:正则表达式的文本字符串

# RegExp.prototype.test()

test() 方法执行一个检索,用来查看正则表达式与指定的字符串是否匹配。返回 truefalse

var reg1 = /\w/;
reg1.test("a");
//true

注意:/g(全局匹配 ) 使用 test 方法,结果不稳定!

# RegExp.prototype.exec()

exec() 方法在一个指定字符串中执行一个搜索匹配。返回一个结果数组或 null

如果你只是为了判断是否匹配(true 或 false),可以使用 RegExp.test() 方法,或者 String.search() 方法。

虚拟DOM

# 虚拟 DOM

本章分析了虚拟 DOM 的使用场景、常用 API、以及 diff 算法 的代码框架。通过学习和了解虚拟 DOM ,为后面的 vue 和 React 学习打好基础。

知识点

5-1 什么是 vdom,为何使用 vdom?

5-2 vdom 的如何应用,核心 API 是什么?

5-3 介绍 一下 diff 算法

Ignorance is the curse of God, knowledge the wing wherewith we fly to heaven.——William Shakespeare

无知乃是罪恶,知识乃是我们借以飞向天堂的翅膀。—— 莎士比亚

# virtual dom

  • vdom 是 vue 和 React 的核心,先讲哪个都绕不开它
  • vdom 比较独立,使用也比较简单
  • 如果面试问到 vue 和 React 的实现,免不了问 vdom

# 问题

  • vdom 是什么?为何会存在 vdom?
  • vdom 的如何应用,核心 API 是什么?
  • 介绍一下 diff 算法

# 5-1 什么是 vdom,为何使用 vdom?

# 知识点

  • 什么是 vdom?
  • 设计一个需求场景
  • 用 jQuery 实现
  • 遇到的问题

# 什么是 vdom?

  • virtual dom,虚拟 DOM
  • 用 JS 模拟 DOM 结构
  • DOM 变化的对比,放在 JS 层来做 ( 图灵完备语言 )
  • 提高重绘性能
<ul id="list">
  <li class="item">Item 1</li>
  <li class="item">Item 2</li>
</ul>

用 JS 模拟 DOM

// vdom
{
    tag:'ul',
    attrs:{
        id:'list'
    },
    children:[
        {
            tag:'li',
            attrs:{className:'item'},
            children:['Item 1']
        },
        {
            tag:'li',
            attrs:{className:'item'},
            children:['Item 2']
        }
    ]
}

# 设计一个需求场景

/*
 * 1.将该数据展示成一个表格
 * 2.随便修改一个信息,表格也跟着修改
 * */
[
  {
    name: "张三",
    age: "20",
    address: "北京",
  },
  {
    name: "李四",
    age: "21",
    address: "上海",
  },
  {
    name: "王五",
    age: "22",
    address: "广州",
  },
];

jQuery 实现

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Title</title>
  </head>
  <body>
    <div id="container"></div>
    <button id="btn-change">change</button>

    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
    <script>
      var data = [
        {
          name: "张三",
          age: "20",
          address: "北京",
        },
        {
          name: "李四",
          age: "21",
          address: "上海",
        },
        {
          name: "王五",
          age: "22",
          address: "广州",
        },
      ];

      //渲染函数
      function render(data) {
        var $container = $("#container");
        //清空容器
        $container.html("");

        //拼接  table
        var $table = $("<table>");
        $table.append($("<tr><td>name</td><td>age</td><td>address</td></tr>"));
        data.forEach(function (item) {
          $table.append(
            $(
              "<tr><td>" +
                item.name +
                "</td><td>" +
                item.age +
                "</td><td>" +
                item.address +
                "</td></tr>"
            )
          );
        });

        //渲染 到页面   jQuery放在这里:只有 一次DOM渲染,性能更好,但是 并不符合理想情况
        $container.append($table);
      }

      $("#btn-change").click(function () {
        data[1].age = 30;
        data[2].address = "深圳";
        //  re-render 再次渲染
        render(data);
      });

      //页面加载完,立即执行
      render(data);
    </script>
  </body>
</html>

mark

# 遇到的问题

  • DOM 操作是 “昂贵” 的,js 运行效率高
  • 尽量减少 DOM 操作 ,而不是 “推倒重来”
  • 项目越复杂 ,影响就越严重
  • vdom 即可解决这个问题

# 问题解答

  • vdom 是什么?为何会存在 vdom?

    • virtual dom,虚拟 DOM
    • 用 JS 模拟 DOM 结构
    • DOM 操作非常 “昂贵”
    • 将 DOM 对比操作放在 JS 层,提高效率
  • vdom 的如何应用,核心 API 是什么?

    • 介绍 snabbdom
    • 重做 之前的 demo
    • 核心 API

# 5-2 vdom 的如何应用,核心 API 是什么?

介绍 snabbdom :一个 vdom 实现库。

# 介绍 snabbdom

虚拟 DOM 非常棒。它允许我们将应用程序的视图表示为其状态的函数。但现有的解决方案太过臃肿,太慢,缺乏功能,API 偏向于 OOP 和 / 或缺少我需要的功能。

Snabbdom 由一个非常简单,高性能和可扩展的核心组成,只有 ≈200SLOC。它提供了模块化架构,具有丰富的功能,可通过自定义模块进 为了保持核心简单,所有非必要功能都委托给模块。

你可以将 Snabbdom 塑造成你想要的任何东西!选择,选择和自定义所需的功能。或者,您可以使用默认扩展并获得具有高性能,小尺寸和下面列出的所有功能的虚拟 DOM 库。

# 介绍 snabbdom - h 函数

mark

# 介绍 snabbdom -patch 函数

mark

# 使用 snabbdom

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Title</title>
  </head>
  <body>
    <div id="container"></div>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.min.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.min.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.min.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.min.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.min.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.min.js"></script>
    <script>
      var snabbdom = window.snabbdom;

      //定义 patch
      var patch = snabbdom.init([
        snabbdom_class,
        snabbdom_props,
        snabbdom_style,
        snabbdom_eventlisteners,
      ]);

      // 定义 h
      var h = snabbdom.h;

      var container = document.getElementById("container");

      //生成 vnode
      var vnode = h("ul#list", {}, [
        h("li.item", {}, "Item 1"),
        h("li.item", {}, "Item 2"),
      ]);

      patch(container, vnode);

      document
        .getElementById("btn-change")
        .addEventListener("click", function () {
          //生成 newVnode
          var newVnode = h("ul#list", {}, [
            h("li.item", {}, "Item 1"),
            h("li.item", {}, "Item B"),
            h("li.item", {}, "Item 3"),
          ]);
          patch(vnode, newVnode);
        });
    </script>
  </body>
</html>

mark

# 重做 之前的 demo

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Title</title>
  </head>
  <body>
    <div id="container"></div>
    <button id="btn-change">change</button>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.min.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.min.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.min.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.min.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.min.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.min.js"></script>
    <script>
      var snabbdom = window.snabbdom;

      //定义 patch
      var patch = snabbdom.init([
        snabbdom_class,
        snabbdom_props,
        snabbdom_style,
        snabbdom_eventlisteners,
      ]);

      // 定义 h
      var h = snabbdom.h;

      //原始数据
      var data = [
        {
          name: "张三",
          age: "20",
          address: "北京",
        },
        {
          name: "李四",
          age: "21",
          address: "上海",
        },
        {
          name: "王五",
          age: "22",
          address: "广州",
        },
      ];

      //把表也放在 data 中
      data.unshift({
        name: "姓名",
        age: "年龄",
        address: "地址",
      });

      var container = document.getElementById("container");

      //渲染函数
      var vnode;

      function render(data) {
        var newVnode = h(
          "table",
          {},
          data.map(function (item) {
            var tds = [];
            var i;
            for (i in item) {
              if (item.hasOwnProperty(i)) {
                tds.push(h("td", {}, item[i] + ""));
              }
            }
            return h("tr", {}, tds);
          })
        );

        if (vnode) {
          // 再次渲染
          patch(vnode, newVnode);
        } else {
          // 初次渲染
          patch(container, newVnode);
        }
        vnode = newVnode;
      }

      // 初次渲染
      render(data);

      var btnChange = document.getElementById("btn-change");
      btnChange.addEventListener("click", function () {
        data[1].age = 30;
        data[2].address = "深圳";
        //re-render
        render(data);
      });
    </script>
  </body>
</html>

mark

# 核心 API

  • h(’<标签名>’,{ … 属性 … },[… 子元素 …])
  • h(’<标签名>’,{ … 属性 … },[ ‘…’])
  • patch(container,vnode)
  • patch(vnode,newVnode)

# 问题解答

  • vdom 的如何应用,核心 API 是什么?
    • 如何使用? 可用 snabbdom 的 用法 来 举例
    • 核心 函数 :h 函数,patch 函数

# 5-3 介绍 一下 diff 算法

# 题目

  • 什么是 diff 算法?
  • 去繁就简
  • vdom 为何用 diff 算法 ?
  • diff 算法 的 实现流程

# 什么是 diff 算法

diff 算法 一直在我们身边

并不是 Vue 和 React 创造出来的概念

diff 算法命令演示

  • linux 系统下: diff log1.txt log2.txt :比较 2 个文件的不同
  • git diff xxxx 的示例 : git 里面比较不同版本间的代码差异

# 去繁就简

  • diff 算法非常复杂,实现难度很大,源码量很大
  • 去繁就简,讲明白核心流程 ,不关心细节
  • 面试官也大部分都不清楚细节,但是很关心核心流程
  • 去繁就简之后,依然 具有很大挑战性,并不简单

# vdom 为何用 diff 算法

  • DOM 操作是 “昂贵” 的,因此尽量减少 DOM 操作
  • 找出本次 DOM 必须更新的节点来更新,其它的 不更新
  • 这个 “找出” 的过程,就需要 diff 算法

# diff 实现过程

  • patch (container,vnode)
  • patch (vnode,newVnode)

# patch (container,vnode)

mark

核心:如何使用 左边的 JS 节点 生成 右侧 Dom 节点?

mark

# patch (vnode,newVnode)

核心:** 对比 **

mark

mark

mark

# diff 实现过程

  • patch (container,vnode)和 patch (vnode,newVnode)
  • createElment
  • updataChildren

# 问题解答

  • 介绍一下 diff 算法?
    • 知道什么是 diff 算法,是 linux 的基础命令
    • vdom 中 应用 diff 算法目的: 是为了 找出需要更新的节点
    • diff 实现:patch (container,vnode)和 patch (vnode,newVnode)
    • 核心 逻辑 , createElment 和 updataChildren

前端面试每日三加一

# 前端面试每日三加一

待更新状态

今天 2019/12/24 ~ ✌

网页版标签分类

# 第 1 天 (2019.09.19)

总览

1、【html】:页面导入样式时,使用 link 和 @import 有什么区别?

解析

区别:

  1. link 是 HTML 标签,@import 是 css 提供的。
  2. link 引入的样式页面加载时同时加载,@import 引入的样式需等页面加载完成后再加载。
  3. link 没有兼容性问题,@import 不兼容 ie5 以下。
  4. link 可以通过 js 操作 DOM 动态引入样式表改变样式,而 @import 不可以。

2、【css】:圣杯布局和双飞翼布局的理解和区别,并用代码实现

解析:

作用:圣杯布局和双飞翼布局解决的问题是一样的,就是两边顶宽,中间自适应的三栏布局,中间栏要在放在文档流前面以优先渲染。

区别:两者都是为了不让左右俩不遮住 middle,经典圣杯布局通过父亲 padding 给左右俩腾位置从而不会遮住 middle 内容,而双飞翼是 middle 设置 margin,限制内部内容区域,从而左右俩遮的地方不会影响到 middle 内容

对于三栏布局,modern solution 是 flex box/grid 布局,这两者可以轻松实现 mobile-friendly 的方案,也可以控制顺序,middle 依然可以先渲染,9012 年兼容性不错了,如果 APP 无视 IE,这是优选

3、【js】:用递归算法实现,数组长度为 5 且元素的随机数在 2-32 间不重复的值

这一题是起源题

描述:

这是一道大题目,把考点拆成了 4 个小项;需要侯选人用递归算法实现(限制 15 行代码以内实现;限制时间 10 分钟内完成):

  1. 生成一个长度为 5 的空数组 arr。
  2. 生成一个(2-32)之间的随机整数 rand。
  3. 把随机数 rand 插入到数组 arr 内,如果数组 arr 内已存在与 rand 相同的数字,则重新生成随机数 rand 并插入到 arr 内 [需要使用递归实现,不能使用 for/while 等循环]
  4. 最终输出一个长度为 5,且内容不重复的数组 arr。
let arr = new Array(5);
let i = 0;
AddRandom(arr, creatrandomnum());

function AddRandom(arr, randomnum) {
  if (arr.indexOf(randomnum) < 0) {
    arr[i] = randomnum;
    i++;
  } else {
    randomnum = creatrandomnum();
  }
  if (i >= 5) {
    console.log(arr);
    return arr;
  } else {
    AddRandom(arr, randomnum);
  }
}

function creatrandomnum() {
  return Math.floor(Math.random() * (32 - 2) + 2); //不含最大值,含最小值 [2-33)
}

点评:
知识点:递归、随机数
难点:1 颗星
这道题主要是想考递归的用法,同时顺带考了生成指定范围的随机数方法。

# 第 2 天 (2019.09.20)

总览

1、【html】 html 的元素有哪些(包含 H5)?

行内元素:
- a
- b
- span
- strong
- i
- em
- button
- input
- label
- br
- textarea
- select

块元素 :
- div
- p
- h1-h6
- ol
- ul
- li
- table
- tbody
- td
- tr
- thead
- dl
- dt
- dd

H5新增元素:
- section
- article
- audio
- video
- hearder
- footer
- small

2、【css】 CSS3 有哪些新增的特性?

前端面试之 CSS3 新特性

边框(borders):
    border-radius 圆角
    box-shadow 盒阴影
    border-image 边框图像
背景:
    background-size 背景图片的尺寸
    background_origin 背景图片的定位区域
    background-clip 背景图片的绘制区域
渐变:
    linear-gradient 线性渐变
    radial-gradient 径向渐变
文本效果;
    word-break
    word-wrap
    text-overflow
    text-shadow
    text-wrap
    text-outline
    text-justify
转换:
2D转换属性
    transform
    transform-origin
2D转换方法
    translate(x,y)
    translateX(n)
    translateY(n)
    rotate(angle)
    scale(n)
    scaleX(n)
    scaleY(n)
    rotate(angle)
    matrix(n,n,n,n,n,n)
3D转换:
*3D转换属性:

    transform
    transform-origin
    transform-style
3D转换方法
    translate3d(x,y,z)
    translateX(x)
    translateY(y)
    translateZ(z)
    scale3d(x,y,z)
    scaleX(x)
    scaleY(y)
    scaleZ(z)
    rotate3d(x,y,z,angle)
    rotateX(x)
    rotateY(y)
    rotateZ(z)
    perspective(n)
过渡
	transition
动画
	@Keyframes规则
	animation
弹性盒子(flexbox)
多媒体查询@media

3、【js】 写一个方法去掉字符串中的空格

写一个方法去掉字符串中的空格,要求传入不同的类型分别能去掉前、后、前后、中间的空格

知识点:正则表达式、数组的 API

var str = "  abc d e f  g ";
function trim(str) {
  var reg = /\s+/g;
  if (typeof str === "string") {
    var trimStr = str.replace(reg, "");
  }
  console.log(trimStr);
}
trim(str);
var trim = function (str) {
  return str.replace(/\s*/g, "");
};
str.replace(/\s*/g, ""); //去除字符串内所有的空格
str.replace(/^\s*|\s*$/g, ""); //去除字符串内两头的空格
str.replace(/^\s*/, ""); //去除字符串内左侧的空格
str.replace(/(\s*$)/g, ""); //去除字符串内右侧的空格
var str = "  abc d e f  g ";
console.log(str.split(" ").join(""));

# 第 3 天 (2019.09.21)

总览

1、【html 】HTML 全局属性 (global attribute) 有哪些(包含 H5)?

全局属性:用于任何HTML5元素的属性

    accesskey :规定激活元素的快捷键;
    class :规定元素的一个或多个类名(引用样式表中的类);
    contenteditable :规定元素内容是否可编辑;
    contextmenu :规定元素的上下文菜单。上下文菜单在用户点击元素时显示。
    data-* :用于存储页面或应用程序的私有定制数据。
    dir :规定元素中内容的文本方向。
    draggable :规定元素是否可拖动。
    dropzone: 规定在拖动被拖动数据时是否进行复制、移动或链接。
    hidden : 样式上会导致元素不显示,但是不能用这个属性实现样式。
    id 规定元素的唯一: id。
    lang :规定元素内容的语言。
    spellcheck: 规定是否对元素进行拼写和语法检查。
    style :规定元素的CSS行内元素。
    tabindex :规定元素的tab键次序。
    title: 规定有关元素的额外信息。
    translate :规定是否应该翻译元素内容。

2、【css】: 在页面上隐藏元素的方法有哪些?

占位:
    -visibility: hidden;
    -margin-left: -100%;
    -opacity: 0;
    -transform: scale(0);

不占位:
    -display: none;
    -width: 0; height: 0; overflow: hidden;

仅对块内文本元素:
    -text-indent: -9999px;
    -font-size: 0;

 利用 position (absolute 的情况下)
    left/right/top/bottom: 9999px/-9999px 让元素在视区外
    z-index: -9999 放到最底层,同一位置可以让其他元素把这个给遮掉

3、【js】去除字符串中最后一个指定的字符

function GetLaststr(s, target) {
  if (typeof s != "string") return false;
  let index = s.lastIndexOf(target);
  return s.substring(0, index) + s.substring(index + 1, s.length);
}
function delLast(str, target) {
  return str
    .split("")
    .reverse()
    .join("")
    .replace(target, "")
    .split("")
    .reverse()
    .join("");
}

# 第 4 天 (2019.09.22)

总览:

1、【html】 HTML5 的文件离线储存怎么使用,工作原理是什么?

有趣的 HTML5:离线存储

优点:
没有网络时可以浏览,加快资源的加载速度,减少服务器负载

使用:
只需要在页面头部加入,然后创建manifest.appcache文件

浏览器如何解析manifest
    1.在线情况:浏览器发现html头部有manifest属性,他会请求manifest文件,如果是第一次访问,那么浏览器会根据manifest文件的内容下载相应的资源并且进行离线存储.如果已经访问过并存储,那么浏览器使用 离线的资源价值,然后对比新的文件,如果没有发生改变就不做任何操作,如果文件改变了,那么就会重新下载文件中的资源并进行离线存储
    2.离线情况:浏览器就直接使用离线存储资源

2、【css】CSS 选择器有哪些?哪些属性可以继承?

选择器:
    通配符,id,class,标签,后代选择器,子选择器,兄弟选择器,属性选择器,伪类选择器,伪元素选择器

可继承的属性:
    字体属性:font-size,font-weight,font-style,font-family
    文本属性:text-indent,text-align,line-height,word-spacing,letter-spacing,color,direction,text-transform
    元素可见性:visibility,opacity
    光标属性:cursor

3、【js】 写一个方法把下划线命名转成大驼峰命名

function changeStr(str) {
  if (str.split("_").length == 1) return;
  str.split("_").reduce((a, b) => {
    return a + b.substr(0, 1).toUpperCase() + b.substr(1);
  });
}

# 第 5 天 (2019.09.23)

总览

1、【html】 简述超链接 target 属性的取值和作用

a 标签的 target 属性一共有四个值。

  • _self

    默认属性。在当前窗口或者框架中加载目标文档。

  • _blank

    打开新的窗口或者新的标签页。在使用这个属性时,最好添加 rel="noopener norefferrer" 属性,防止打开的新窗口对原窗口进行篡改。防止 window.opener API 的恶意行为。

  • _parent

    frame 或者 iframe 中使用较多。在父级框架中载入目标文档,当 a 标签本身在顶层时,与 _self 相同。

  • _top

    frame 或者 iframe 中使用较多。直接在顶层的框架中载入目标文档,加载整个窗口。

2、【css】CSS3 新增伪类有哪些并简要描述

CSS3 中规定伪类使用一个 : 来表示;伪元素则使用 :: 来表示。

CSS3 中新增的伪元素有以下这些:

  • :first-child / :last-child 表示子元素结构关系的
  • :nth-child() / nth-last-child() 用来控制奇数、偶数行的(控制表单奇数、偶数行的样式)
  • :first-of-type / :last-of-type 表示一组兄弟元素中其类型的第一个元素 MDN
  • :nth-of-type() / :nth-last-of-type() 这个选择器匹配那些在相同兄弟节点中的位置与模式 an+b 匹配的相同元素 ` MDN
  • :root html 根元素
  • :not() 否定选择器,用的比较多
  • :only-child 只有一个子元素时才会生效
  • :empty 选择连空格都没有的元素

3、【js】写一个把字符串大小写切换的方法

正则表达式:

function caseConvert(str) {
  return str.replace(/([a-z]*)([A-Z]*)/g, (m, s1, s2) => {
    return `${s1.toUpperCase()}${s2.toLowerCase()}`;
  });
}

利用 toUpperCase () :

let str = "aBcDeFgH";
let arr = [];
for (let item of str) {
  if (item === item.toUpperCase()) {
    item = item.toLowerCase();
  } else {
    item = item.toUpperCase();
  }
  arr.push(item);
}
let newStr = arr.join("");

# 第 6 天 (2019.09.24)

总览:

1、【html】label 都有哪些作用?并举相应的例子说明

解析:

  1. 互相关联的机制

表示用户界面中某个元素的说明
增加命中区域,屏幕阅读器可以读出标签。使使用辅助技术的用户更容易理解输入 哪些数据

  1. 利用 label "模拟" button 来解决不同浏览器原生 button 样式不同的问题
  2. 结合 checkboxradio 表单元素实现纯 CSS 状态切换,这样的实例就太多了。比如控制 CSS 动画播放和停止。下面是一部分代码。详细实例地址 *
  3. inputfocus 事件会触发锚点定位,我们可以利用 label 当触发器实现选项卡切换效果。下面代码选自张鑫旭《CSS 世界》

2、【css】用 css 创建一个三角形,并简述原理

#triangle {
  width: 0;
  height: 0;
  margin: 100px auto;
  border-top: 50px solid transparent;
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  border-bottom: 50px solid red;
}

3、【js】写一个去除制表符和换行符的方法

function fn(str) {
  var s = str.replace(/\t\n\v\r\f+/g, "");
  return s;
}

# 第 7 天 (2019.09.25)

总览

1、【html】iframe 框架都有哪些优缺点?

iframe 是一种框架,也是一种很常见的网页嵌入方式

iframe 的优点:

1.iframe 能够原封不动的把嵌入的网页展现出来。

2. 如果有多个网页引用 iframe,那么你只需要修改 iframe 的内容,就可以实现调用的每一个页面内容的更改,方便快捷。

3. 网页如果为了统一风格,头部和版本都是一样的,就可以写成一个页面,用 iframe 来嵌套,可以增加代码的可重用。

4. 如果遇到加载缓慢的第三方内容如图标和广告,这些问题可以由 iframe 来解决。

iframe 的缺点

1. 会产生很多页面,不容易管理。

2.iframe 框架结构有时会让人感到迷惑,如果框架个数多的话,可能会出现上下、左右滚动条,会分散访问者的注意力,用户体验度差。

3. 代码复杂,无法被一些搜索引擎索引到,这一点很关键,现在的搜索引擎爬虫还不能很好的处理 iframe 中的内容,所以使用 iframe 会不利于搜索引擎优化。

4. 很多的移动设备(PDA 手机)无法完全显示框架,设备兼容性差。

5.iframe 框架页面会增加服务器的 http 请求,对于大型网站是不可取的。

分析了这么多,现在基本上都是用 Ajax 来代替 iframe,所以 iframe 已经渐渐的退出了前端开发

2.【css】简述你对 BFC 规范的理解

[布局概念] 关于 CSS-BFC 深入理解

块格式化上下文(Block Formatting Context,BFC) 是 Web 页面的可视化 CSS 渲染的一部分,是块盒子的布局过程发生的区域,也是浮动元素与其他元素交互的区域。

BFC:是CSS中的一个渲染机制,BFC就相当于一个盒子,内部的元素与外界的元素互不干扰。它不会影响外部的布局,外部的布局也不会影响到它。

形成条件(任意一条)
    float的值不是none
    position 的值不是static或者relative
    display的值是inline-block,table-cell,flex,table-caption或者inline-flex
    overflow的值不是visible

特性
    内部的盒子会在垂直方向上一个接一个的放置
    对于同一个BFC的俩个相邻的盒子的margin会发生重叠,与方向无关。
    每个元素的左外边距与包含块的左边界相接触(从左到右),即使浮动元素也是如此
    BFC的区域不会与float的元素区域重叠
    计算BFC的高度时,浮动子元素也参与计算
    BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素,反之亦然

3、【js】 统计某一字符或字符串在另一个字符串中出现的次数

function strCount(str, target) {
    let count = 0
    if (!target) return count
    while(str.match(target)) {
        str = str.replace(target, '')
        count++
    }
    return count
}

# 第 8 天 (2019.09.26)

总览:

1、【html】 简述下 html5 的离线储存原理,同时说明如何使用?

原理:
HTML5的离线存储是基于一个新建的.appcache文件的缓存机制(不是存储技术),通过这个文件上的解析清单离线存储资源,这些资源就会像cookie一样被存储了下来。之后当网络在处于离线状态下时,浏览器会通过被离线存储的数据进行页面展示。
如何使用: ① 页面头部像下面一样加入一个manifest的属性。

<!DOCTYPE html>
<html manifest="cache.manifest">
  ...
</html>
在cache.manifest文件的编写离线存储的资源。 CACHE MANIFEST #v0.1 CACHE:
js/index.js css/index.css NETWORK: images/logo.png FALLBACK: *.html /404.html /*
/ /404.html 或 /html/ /404.html 也可*/
以#号开头的是注释,一般会在第二行写个版本号,用来在缓存的文件更新时,更新manifest以实现浏览器重新下载新的文件,可以是版本号,时间戳或md5码等。
离线存储的 manifest一般由三个部分组成:
① CACHE:必选,表示需要离线存储的资源列表,由于包含manifest文件的页面将被自动离线存储,所以不需要把页面自身也列出来。
② NETWORK:可选,可以使用通配符,表示在它下面列出来的资源只有在在线的情况下才能访问,他们不会被离线存储,所以在离线情况下无法使用这些资源。不过,如果在CACHE和NETWORK中有一个相同的资源,那么这个资源还是会被离线存储,也就是说CACHE的优先级更高。
③ FALLBACK:可选,表示如果访问第一个资源失败,那么就使用第二个资源来替换他,如/html/
/404.html表示用 “404.html” 替代 /html/ 目录中的所有文件,/ /404.html表示用
“404.html” 替代当前目录中的所有文件,*.html /404.html表示用 “404.html” 替代
所有html文件。

2、【css】清除浮动的方式有哪些及优缺点?

唠叨:

  • 在现在的实际工作当中我已经很少用浮动来布局了,真的很少,刚开始学习的时候用的还蛮多,现在 Flex 布局,标准文档流以及 定位 已经可以满足大部分的布局需求了。
  • 浮动带来的问题是盒子塌陷问题,所以我们就来解决这个问题吧

解决方案

  1. 给外部盒子也添加浮动

把外部盒子也从标准文档流中抽离,让它和孩子们见面。
缺点 :可读性差,不易于维护(别人很难理解为什么要给父元素也添上 float),而且可能需要调整整个页面布局。

  1. 在外部盒子内最下方添上带 clear 属性的空盒子

可以是 div 也可以是其它块级元素,把 <div style="clear:both;"></div> 放在盒内底部,用最下面的空盒子清除浮动,把盒子重新撑起来。
缺点:引入了冗余元素

  1. 用 overflow:hidden 清除浮动 ,外层父元素使用 overflow:hidden; 属性触发 BFC,让内层的 float 不会影响外层的布局

给外部盒子添上这个属性就好了,非常简单。
缺点 :有可能造成溢出元素不可见,影响展示效果。

  1. 用 after 伪元素清除浮动 (比较常用的方式)

给外部盒子的 after 伪元素设置 clear 属性,再隐藏它
这其实是对空盒子方案的改进,一种纯 CSS 的解决方案,不用引入冗余元素。

3、【js】 简要描述下什么是回调函数并写一个例子出来

回调是把一个函数作为参数传递给另一个函数,当该函数满足某个条件时触发该参数函数。
主要用于异步操作 例如网络请求 防止页面同步代码阻塞导致渲染线程停止

function longTask(callback, timeout) {
  setTimeout(callback, timeout);
}
longTask(() => {
  console.log("回调任务被执行了");
}, 2000);
console.log("我是同步代码 不会阻塞我");

# 第 9 天 (2019.09.27)

总览:

1、【html】浏览器内多个标签页之间的通信方式有哪些?

实现多个标签页之间通信的几种方法

完全答案:
    WebSocket (可跨域)
    postMessage(可跨域)
    Worker之SharedWorker
    Server-Sent Events
    localStorage
    BroadcastChannel
    Cookies

2、【css】 简述下你理解的优雅降级和渐进增强

前端面试题 - 渐进增强和优雅降级

简介:渐进增强和优雅降级这两个概念是在 CSS3 出现之后火起来的。由于低级浏览器不支持 CSS3,但是 CSS3 特效太优秀不忍放弃,所以在高级浏览器中使用 CSS3,而在低级浏览器只保证最基本的功能。

优雅降级:
	先不考虑兼容,优先最新版本浏览器效果,之后再逐渐兼容低版本浏览器。

渐进增强:
    考虑兼容,以较低(多)浏览器效果为主,之后再逐渐增加对新版本浏览器的支持,以内容为主。也是多数公司所采用的方法。

3、【js】 写一个判断数据类型的方法

考点:Object.prototype.toString 方法

const typeCheck = (obj) => {
  const typeStr = Object.prototype.toString.call(obj);
  console.log(typeStr);
  return typeStr.toLowerCase().slice(8, typeStr.length - 1);
};

# 第 10 天 (2019.09.28)

总览

1、【html】 viewport 常见设置都有哪些?

解析

在移动端做开发时,必须要搞清楚 viewport 这一设置。

viewport 就是视区窗口,也就是浏览器中显示网页的部分。PC 端上基本等于设备显示区域,但在移动端上 viewport 会超出设备的显示区域(即会有横向滚动条出现)。
设备默认的 viewport 在 980 - 1024 之间。

为了让移动端可以很好地显示页面,因此需要对 viewport 进行设置。相关的设置值如下:

mark

// width=device-width, initial-scale=1.0 是为了兼容不同浏览器
<meta
  name="viewport"
  content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
/>

2、【css】 对比下 px、em、rem 有什么不同?

  • px: 绝对固定的值,无论页面放大或者缩小都不会改变。
  • em: 相对父元素字体大小的倍数。如果父元素的字体为 12px ,那么子元素 1em 就是 24px 。由于是相对父级的倍数,所以多层嵌套时,倍数关系的计算会很头痛。
  • rem: 相对根元素字体大小的倍数。相对于 html 的字体大小,如果不做任何修改,浏览器默认字体大小为 16px

小技巧

如果为了方便计算 rem ,可以设置 font-size= 62.5% 这样一来默认的字体就变成 10px 了。之后的 rem 就是以 10 为基准了。

3、【js】 简要描述下什么是回调函数并写一个例子出来

回调函数首先作为一个函数的参数传入,当这个函数执行后再执行的函数,往往会依赖前一个函数执行的结果。
javascript 中,对于 I/O、HTTP 请求等异步操作,为了控制执行的顺序就需要使用回调的方法。

// 第三个参数就是回调函数
function func1(param1, param2, ..., callback){
  // To do some action
  // 往往会在最后调用 callback 并且传入操作过的参数
  callback(cbParam1, cbParam2, ...)
}

// 实际调用的时候
func1(param1, param2, ..., (cbParam1, cbParam2, ...) => {
  // To do some action
})

当有过个任务需要顺序执行时,如果采用回调函数的形式就会出现我们熟悉的 “回调地狱” 的情况。为了解决这个问题,在 ES6 中就有了 Promiseasync/await 方法。
目前看来 async/await 在异步写法上较为优雅。

# 第 11 天 (2019.09.29)

总览:

1、【html】 你对标签语义化的理解是什么?

①去掉或者丢失样式的时候能够让页面呈现出清晰的结构;

②有利于SEO:和搜索引擎建立良好沟通,有助于爬虫抓取更多的有效信息:爬虫依赖于标签来确定上下文和各个关键字的权重;

③方便其他设备解析(如屏幕阅读器、盲人阅读器、移动设备)以意义的方式来渲染网页;

④便于团队开发和维护,语义化更具可读性,是下一步吧网页的重要动向,遵循W3C标准的团队都遵循这个标准,可以减少差异化。

2、【css】css 常用的布局方式有哪些?

  • 流式布局:最基本的布局,就是顺着 html 像流水一样流下来

  • 绝对定位:利用 position: absolute 进行绝对定位的布局

  • float 布局:最初用来解决多栏布局的问题。比如圣杯、双飞燕的布局都可以用 float 来实现

  • 珊格布局: bootstrap 用的布局,把页面分为 24 分,通过 row 和 col 进行布局

  • flex 布局: css3 的布局可以非常灵活地进行布局和排版

  • grid 布局:网格布局

    3.【js】简要描述下 JS 有哪些内置的对象?

JS 所有内置对象属性和方法汇总

JavaScript 有 3 大对象,分别是本地对象、内置对象和宿主对象。

本地对象:这些引用类型在运行过程中需要通过new来创建所需的实例对象。
包含:Object、Array、Date、RegExp、Function、Boolean、Number、String等。

内置对象:内置对象是本地对象的子集。
包含:Global和Math。
ECMAScript5中增添了JSON这个存在于全局的内置对象。

宿主对象:对于嵌入到网页中的JS来说,其宿主对象就是浏览器提供的对象,浏览器对象有很多,如Window和Document等。
所有的DOM和BOM对象都属于宿主对象。

# 第 12 天 (2019.09.30)

总览

1、【html】常见的浏览器内核都有哪些?并介绍下你对内核的理解

常见的浏览器内核:

Trident内核:IE,360,搜过浏览器;
Gecko内核:Netscape6及以上版本,
Presto内核:Opera
Blink内核:Opera;
Webkit内核:Safari,Chrome

介绍一下对浏览器内核的理解

主要分成两个部分:渲染引擎 (Render Engine) 和 JS 引擎。

渲染引擎:负责取得网页的内容 (html,xml 和图像等),整理讯息 (例如假如 css),以及计算网页的显示方式,然后输出到显示器或打印机。浏览器的内核的不同对于网页的语法解释会有不同,所以渲染的效果也不同。所有网页浏览器、电子邮件客户端以及它需要编辑、显示网络内容的应用程序都需要内核。

JS 引擎:解析和执行 JavaScript 来实现网页的动态效果。

最开始渲染引擎和 JS 引擎并没有区分的很明确,后来 JS 引擎越来越独立,内核就倾向与只指渲染引擎。

2.【css】说说你对 css 盒子模型的理解?

面试官:谈谈你对 CSS 盒模型的认识?(你确定会?)

涉及知识点 (层层递进):

  1. 基本概念:标准模型 + IE 模型 (区别)
  2. CSS 如何设置这两种模型
  3. JS 如何设置获取盒子模型对应的宽和高
  4. 实例题 (根据盒模型解释边距重叠)
  5. BFC (边距重叠解决方案)

1. 基本概念:标准模型 + IE 模型

标准盒子模型:包括 margin,border,padding,content,并且 content 部分不包括其他部分
IE 盒子模型:包括 margin,border,padding,content,content 包含了 border 和 padding

2.css 如何设置这两种模式

标准盒模型:box-sizing:content-box
IE 盒模型:box-sizing:border-box

3.js 如何设置获取盒子模型对应的宽和高

  1. dom.style.width:

只能获取内联样式,因此是不准确的

  1. dom.currentStyle.width

与 window.getComputedStyle 方法功能相同,实现在旧版本的 IE 浏览器中
3)window.getComputedStyle (dom).width:
方法返回一个对象,该对象在应用活动样式表并解析这些值可能包含的任何基本计算后报告元素的所有 CSS 属性值。因此输出是准确的
4)dom.getBoundingClientRect ().width
返回一个 DOMRect 对象,这个对象是由该元素的 getClientRects () 方法返回的一组矩形的集合。
DOMRect 对象包含了一组用于描述边框的只读属性–left,top,right,bottom, 单位为像素。除了 width 和 height 外的属性都是相对于视口的左上角位置而言的。

3、【js】 写一个获取当前 url 查询字符串中的参数的方法

/*
	例如网址:http://zxinc520.com/?a=hello&b=world
	window.location.search = " ?a=hello&b=world "
*/

function params() {
  let search = window.location.search;
  search = search.substr(1, search.length);
  const res = {};
  if (!search) return res;
  search.split("&").map((item) => {
    const [key, value] = item.split("=");
    res[key] = decodeURIComponent(value);
  });
  return res;
}

4、【软技能】 网页应用从服务器主动推送到客户端有那些方式?

1. html5 websocket
2. WebSocket 通过 Flash
3. XHR长时间连接
4. XHR Multipart Streaming
5. 不可见的Iframe
6. <script>标签的长时间连接(可跨域)

# 第 13 天 (2019.10.08)

总览:

1、【html】 html5 中的 form 怎么关闭自动完成?

h5新增的补全功能,菜鸟教程上写的比较含糊比较难懂;
解释: 在部分浏览器上,foucs输入框可以把之前输入过的值自动填入,如果不想自动填入,可以关掉它;
autocomplete="off"
默认是"on" 开启状态

一般业务下不会调整这个自动完成,因为对产品来说简化用户操作,建议打开

2、【css】::before 和:after 中单冒号和双冒号的区别是什么,这两个伪元素有什么作用?

:表示伪类,是一种样式,比如:hover, :active等
::表示伪元素,是具体的内容,比如::before是在元素前面插入内容,::after则是在元素后面插入内容,不过需要content配合,并且插入的内容是inline的。
:before和:after其实还是表示伪元素,在css3中已经修订为::before和::after了,只是为了能兼容IE浏览器,所以也可以表示成:before和:after
:: 和 : 是 CSS3 中为了区别伪类和伪元素所用的不同的写法。:: 表示伪元素,目前两种写法都被兼容。
::before,::after 可以在一个 DOM 元素的前面和后面增加一个伪元素。可以用来清除浮动、为元素增加特殊效果(如前面有特殊符号等)。
::before 和 ::after 默认添加的是 inlne 元素,通过 content 属性来设置展示的内容,并且必须要设置 content 属性。content 属性可以利用 attr 与元素的相关内容做联动。

3、【js】说说你对 javascript 的作用域的理解?

1、全局作用域。这个没啥说的,就是在顶层环境中申明的变量都是全局作用域,他们的属性其实都在window对象下面。

2、函数作用域。在函数内部定义的变量都是函数作用域,只能在函数中访问到,当然也可以通过闭包来访问。除此之外,在其他地方是没法访问函数内部的变量的。
局部作用域。es6中新增的特性,弥补了以前只能使用匿名及时运行函数来创建局部变量的缺陷。使用很简单,直接用let来申明变量就行。也可以使用const来申明变量,表明这是常数。

3、作用域链。要说清这个,需要首先明白javascript的代码运行过程。假设现在有个函数funcA,在该函数内部申明了一个局部变量a,在函数内部又定义了一个函数funcB,在函数B中申明了变量b。如下:
    function funcA () {
    let a;
    function funcB () {
    let b;
    }
    }

当进入funcA时,这时候会把变量a压入当前的作用域A中,并且将作用域A入栈,当进入funcB时,则会把变量b压入当前的作用域B中,并且将作用域B入栈,那么这时候栈中就有了作用域A和作用域B,当在funcB中查找某个变量时,会先从当前的作用域B中查找,如果没有的话,那么就根据栈中的作用域依次往上查找,这就是作用域链。

4、【软技能】http 都有哪些状态码?

200 成功
301 重定向
304 (未修改) 自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容。
400 (错误请求) 服务器不理解请求的语法。
403 (禁止) 服务器拒绝请求。
404 (未找到) 服务器找不到请求的网页。
500 (服务器内部错误) 服务器遇到错误,无法完成请求。
501 (尚未实施) 服务器不具备完成请求的功能。 例如,服务器无法识别请求方法时可能会返回此代码。
502 (错误网关) 服务器作为网关或代理,从上游服务器收到无效响应。
503 (服务不可用) 服务器目前无法使用(由于超载或停机维护)。 通常,这只是暂时状态。
504 (网关超时) 服务器作为网关或代理,但是没有及时从上游服务器收到请求。
505 (HTTP 版本不受支持) 服务器不支持请求中所用的 HTTP 协议版本。

常见状态码:
    2xx 成功
    3xx 重定向
    4xx 未找到资源
    5xx 服务器异常

# 第 14 天 (2019.10.09)

总览

1、【html】为什么 HTML5 只需要写 <!DOCTYPE HTML > 就可以?

解析

<!DOCTYPE>只是一个说明,用来告诉浏览器当前的html页面是用什么版本的html写的。
html4.01的<!DOCTYPE>引用了DTD(document type define),因为html4.01是基于SGML的,而它引用的DTD指明了html的规则,从而浏览器能正确的渲染页面。而html5不是基于SGML所以不需要引用DTD。

翻译一下:SGML,即一般标准标记语言,是一个用于定义文档标记语言标准的集合。
总结一下:因为html4是基于SGML这个标记语言的集合,既然是集合说明里面有各种的标准,那么DTD就是指出了当前html文件是用的是哪个SGML规则。
html5不存在这个问题,所以只需要简单的声明浏览器就可以正确渲染页面啦

2、【css】 position:fixed; 在 ios 下无效该怎么办?

当采用 fixed 做吸底、吸顶布局时,如果触发键盘弹出事件则 fixed 属性会失效,布局就会被扰乱。其原因解释如下:

软键盘唤起后,页面的 fixed 元素将失效(即无法浮动,也可以理解为变成了 absolute 定位),所以当页面超过一屏且滚动时,失效的 fixed 元素就会跟随滚动了。

第三方库 isScroll.js 可以解决此问题。

3、【js】 什么是闭包?优缺点分别是什么?

解析

闭包是可以访问另一个函数作用域的函数。由于 javascript 的特性,外层的函数无法访问内部函数的变量;而内部函数可以访问外部函数的变量(即作用域链)。

function a() {
  var b = 1;
  var c = 2;
  // 这个函数就是个闭包,可以访问外层 a 函数的变量
  return function () {
    var d = 3;
    return b + c + d;
  };
}

var e = a();
console.log(e());

因此,使用闭包可以隐藏变量以及防止变量被篡改和作用域的污染,从而实现封装。
而缺点就是由于保留了作用域链,会增加内存的开销。因此需要注意内存的使用,并且防止内存泄露的问题。

4、【软技能】 你最喜欢用哪些编辑器?喜欢它的理由是什么?

解析:

webstorm : 喜欢它不需要理由!

# 第 15 天 (2019.10.10)

总览:

1、【html】title 与 h1 的区别、b 与 strong 的区别、i 与 em 的区别?

关于 title 和 h1,title 是网页的标题。主要面向的对象是搜索引擎和通过搜索结果过来的人(面向外人,可以理解为报纸首页的标题)。而 h1 是网页内部的标题,是给已经进到页面的人看的(可以理解为报纸某个版面的大标题)。从人类的语境上来理解,两者并没有差别。

b 与 strong 的效果人眼上是无法区分的。在语义上,b 仅表示加粗既装饰用,我们应该使用 CSS 而不应该使用 b;而 strong 则表示被包围的内容很重要,是语气上的感觉。对于搜索引擎来说,会把 b 和 strong 视为同一含义。因此我们在使用上需要注意。

i 与 em 的区别类似 b 和 strong 的区别。i 用于斜体展示,我们应该使用 CSS 而不应该使用 i;而 em 则是对内容的强调,但程度没有 strong 那么高。同样,对搜索引擎来说,两者是没有区别的。

2、【css】style 标签写在 body 前和 body 后的区别是什么?

参考文章:
Will it be a wrong idea to have in <body >? > W3C The style element > 什么是 FOUC?如何避免 FOUC? > Understanding the Critical Rendering Path

在 HTML4 的时候,不应该把 style 放到 body 中间。

浏览器在渲染页面时 DOM 和 CSSOM 是并行的,然后两者结合形成 Render Tree 显示页面。从直觉上来说,style 写在 body 前不会对 DOM 的渲染进行阻塞;而写在 body 内会对 DOM 渲染进行阻塞。会产生 FOUC(Flash of Unstyled Content) 的现象,既一瞬间的白屏或者样式的突然变化(原因是 Render Tree 重新生成了)。

不过 W3C 在 HTML5.2 的定义中对于 style 标签的使用的定义中是允许将 style 放到 body 中的。

Contexts in which this element can be used:
Where metadata content is expected.
In a noscript element that is a child of a head element.
In the body, where flow content is expected.

3、【js】写一个数组去重的方法(支持多维数组)

5 种方法实现数组扁平化

7 种方法实现数组去重

var arr = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9, 3, 2],
];
console.log(Array.from(new Set(arr.toString().split(",").map(Number))));
// [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]

4、【软技能】对于加班你是怎么看的?

  1. 首先,始终要以工作效率为首要目标,不能出现为了加班而故意降低白天的工作效率。
  2. 其次,在保证了白天的工作效率以后,如果确实需要加班,则可以适度的加班,但不能超过 10 点,不然肯定影响第二天的效率。

# 第 16 天 (2019.10.11)

总览:

1、【html】元素的 alt 和 title 有什么区别?

ALT 属性:

最常见用在 <img> 标签上,那我们先来看下 <img> 标签的 alt 属性。

alt 属性是一个必需的属性,它规定在图像无法显示时的替代文本。

假设由于下列原因用户无法查看图像, alt 属性可以为图像提供替代的信息:

  • 网速太慢
  • src 属性中的错误
  • 浏览器禁用图像
  • 用户使用的是屏幕阅读器

<img >  标签的  alt  属性指定了替代文本,用于在图像无法显示或者用户禁用图像显示时,代替图像显示在浏览器中的内容

**TITLE 属性: **

title 属性规定关于元素的额外信息。

这些信息通常会在鼠标移到元素上时显示一段工具提示文本(tooltip text)。

提示: title 属性常与 form 以及 a 元素一同使用,以提供关于输入格式和链接目标的信息。同时它也是 abbracronym 元素的必需属性。当然 title 属性是比较广泛使用的,可以用在除了 basebasefontheadhtmlmetaparamscripttitle 之外的所有标签。但是并不是必须的。

title 属性有一个很好的用途,即为链接添加描述性文字,特别是当连接本身并不是十分清楚的表达了链接的目的。这样就使得访问者知道那些链接将会带他们到什么地方,他们就不会加载一个可能完全不感兴趣的页面。另外一个潜在的应用就是为图像提供额外的说明信息,比如日期或者其他非本质的信息。

2、【css】请描述 margin 边界叠加是什么及解决方案

1,使用padding代替,但是父盒子要减去相应的高度
2,使用boder(透明)代替(不推荐,不符合书写规范,如果父盒子子盒子时有颜色的不好处理)
3,给父盒子设置overflow:hidden(如果有移除元素无法使用)
4,给父盒子设置1px的padding
5,给父盒子设置1px的透明border,高度减1px
6,子盒子使用定位position
7,子盒子浮动, 但是居中比较难以控制
8,给子盒子设置display: inline-block;
9,子盒子上面放一个table标签

3、【js】 返回到顶部的方法有哪些?把其中一个方法写出来

  1. 锚点

使用锚点链接是一种简单的返回顶部的功能实现。该实现主要在页面顶部放置一个指定名称的锚点链接,然后在页面下方放置一个返回到该锚点的链接,用户点击该链接即可返回到该锚点所在的顶部位置

<div id="topAnchor"></div>
<a href="#topAnchor">回到顶部</a>
  1. scrollTop

scrollTop 属性表示被隐藏在内容区域上方的像素数。元素未滚动时,scrollTop 的值为 0,如果元素被垂直滚动了,scrollTop 的值大于 0,且表示元素上方不可见内容的像素宽度

由于 scrollTop 是可写的,可以利用 scrollTop 来实现回到顶部的功能

[注意] 关于页面的 scrollTop 的兼容问题详细内容移步至此

function scrollTop() {
  if ((document.body.scrollTop || document.documentElement.scrollTop) != 0) {
    document.body.scrollTop = document.documentElement.scrollTop = 0;
  }
}
btn.onclick = scrollTop;
  1. scrollTo()window.scroll()
window.scrollTo(0, 1000);

// 设置滚动行为改为平滑的滚动
window.scrollTo({
  top: 1000,
  behavior: "smooth",
});
window.scroll({
  top: 100,
  left: 100,
  behavior: "smooth",
});
  1. Window.scrollBy()

在窗口中按指定的偏移量滚动文档。

向下滚动一页:

window.scrollBy(0, window.innerHeight);

向上滚动一页:

window.scrollBy(0, -window.innerHeight);

使用 options:

window.scrollBy({
  top: 100,
  left: 100,
  behavior: "smooth",
});

4.【软技能】你在的公司有没有做代码审查(CodeReview)?如果有是怎么做的?如果没有你觉得应该怎么做才更好?

1、有独立的代码审查部门,定期发送邮件给相关人员,里面有本部门全部项目的代码质量统计,在代码过差时依次向上级发通知
2、依据每个组内风格,有的组在每次合并生产环境都会review
3、总的来说代码审查是好事,但如果出现咸鱼池塘以及产品流程不规范导致迭代需求过多而不合理,会造成很多困扰,自身也可能流于形式,一定要结合实际情况来看

# 第 17 天 (2019.10.13)

总览:

1、【html】你认为 table 的作用和优缺点是什么呢?

解析:

优点:样式简单,构建方便,兼容良好
缺点:在于会多处非常多的 DOM 节点(想想一个 td 里面再来一个 table),会导致页面加载变慢、影响加载和渲染,维护麻烦,不利于 SEO(table 原本就不是用来布局的)。也因此,在 CSS 成熟之后,table 布局马上就变成历史了。

2、【css】解释下 CSS sprites 的原理和优缺点分别是什么?

CSS Sprites 其实就是把网页中一些背景图片整合到一张图片文件中,再利用 CSS 的 “background-image”,“background- repeat”,“background-position” 的组合进行背景定位,background-position 可以用数字精确的定位出背景图片的位置。

优点:

  • 减少网页的 http 请求,大大的提高页面的性能
  • 减少图片的字节
  • 解决了网页设计师在图片命名上的困扰
  • 更换风格方便,维护起来更加方便

缺点:

  • 在图片合并的时候,要留好足够的空间,防止板块内出现不必要的背景;最痛苦的是在宽屏,高分辨率的屏幕下的自适应页面,你的图片如果不够宽,很容易出现背景断裂;
  • CSS Sprites 在开发的时候比较麻烦,通过 photoshop 或其他工具测量计算每一个背景单元的精确位置

3、【js】typeof (‘abc’) 和 typeof 'abc’都是 string, 那么 typeof 是操作符还是函数?

typeof操作符,不是函数。可以添加括号,但是括号的作用是进行分组而非函数的调用。

参考自 <JavaScript 高级程序设计>

4、【软技能】 说说你对 SVN 和 GIT 的理解和区别 ?

解析: 话说 Svn 与 Git 的区别

  • 最核心的区别 :Git 是分布式 SCM,而 SVN 是基于服务器的,也就是说每个开发者本地都有一套 git 库,每个人维护自己的版本(或者合并其他人的版本),而 SVN 是每个人写完代码后都及时的 checkin 到服务器上,进行合并。

  • Git 把内容按元数据方式存储,而 SVN 是按文件

  • Git 没有一个全局版本号,而 SVN 有:目前为止这是跟 SVN 相比 Git 缺少的最大的一个特征。

  • Git 的内容的完整性要优于 SVN: GIT 的内容存储使用的是 SHA-1 哈希算法。这能确保代码内容的完整性,确保在遇到磁盘故障和网络问题时降低对版本库的破坏。

  • Git 下载下来后,在 OffLine 状态下可以看到所有的 Log,SVN 不可以。

  • 刚开始用时很狗血的一点,SVN 必须先 Update 才能 Commit, 忘记了合并时就会出现一些错误,git 还是比较少的出现这种情况。

  • 克隆一份全新的目录以同样拥有五个分支来说,SVN 是同时复製 5 个版本的文件,也就是说重复五次同样的动作。而 Git 只是获取文件的每个版本的 元素,然后只载入主要的分支 (master) 在我的经验,克隆一个拥有将近一万个提交 (commit), 五个分支,每个分支有大约 1500 个文件的 SVN, 耗了将近一个小时!而 Git 只用了区区的 1 分钟!

  • 版本库(repository):SVN 只能有一个指定中央版本库。当这个中央版本库有问题时,所有工作成员都一起瘫痪直到版本库维修完毕或者新的版本库设立完成。而 Git 可以有无限个版本库。

最后总结一下:

SVN 的特点是简单,只是需要一个放代码的地方时用是 OK 的。

Git 的特点版本控制可以不依赖网络做任何事情,对分支和合并有更好的支持 (当然这是开发者最关心的地方),不过想各位能更好使用它,需要花点时间尝试下。

# 第 18 天 (2019.10.14)

总览:

1、【html】怎样在页面上实现一个圆形的可点击区域?

  • DOM 元素配合 border-radius: 50% 即可实现圆形点击区域。例子
  • 利用 <map><area> 标签设置圆形点击区域。参考文章:HTML 标签及在实际开发中的应用
  • 利用 SVG 作出圆形,然后添加点击事件。
  • 如果在 canvas 上,就需要画出圆形,然后计算鼠标的坐标是否落在圆内。

2、【css】什么是 FOUC?你是如何避免 FOUC 的?

解析: 什么是 FOUC?如何避免 FOUC?

什么叫做 FOUC 浏览器样式闪烁

如果使用import方法对css进行导入,会导致某些页面在Windows 下的Internet Explorer出现一些奇怪的现象

以无样式显示页面内容的瞬间闪烁,

这种现象称之为文档样式短暂失效(Flash of Unstyled Content),简称为FOUC.

原因大致为:

  1. 使用 import 方法导入样式表。
  2. 将样式表放在页面底部
  3. 有几个样式表,放在 html 结构的不同位置。

其实原理很清楚:当样式表晚于结构性 html 加载,当加载到此样式表时,页面将停止之前的渲染。

此样式表被下载和解析后,将重新渲染页面,也就出现了短暂的花屏现象。

解决方法 :使用 link 标签将样式表放在文档 head 中

3、【js】 你理解的 "use strict"; 是什么?使用它有什么优缺点?

解析: JavaScript 严格模式 (use strict)

JavaScript 严格模式(strict mode)即在严格的条件下运行。

严格模式,其实就是更严格了

设立 "严格模式" 的目的,主要有以下几个:

  • 消除 Javascript 语法的一些不合理、不严谨之处,减少一些怪异行为;
  • 消除代码运行的一些不安全之处,保证代码运行的安全;
  • 提高编译器效率,增加运行速度;
  • 为未来新版本的 Javascript 做好铺垫。

我放几个常见的吧,详情可以去下面的文章中看

  1. 禁止 this 关键字指向全局对象 (严格模式下的 全局中的 this 是 undefined 不是 window)
  2. 禁止在函数内部遍历调用栈
  3. 全局变量必须显式声明
  4. arguments 不再追踪参数的变化
(function () {
  "use strict";
  b = 1; //Uncaught ReferenceError: b is not defined
})();

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode
http://www.ruanyifeng.com/blog/2013/01/javascript_strict_mode.html

4、【软技能】你如何看待团建的?你们团建一般都怎么实施?

公司希望团建加强团队的凝聚力,大家可能在想:怎么可能能加强,吃喝玩乐,玩几个游戏就可以加强了?其实公司加强的是对公司有认可度的那群人的凝聚力,而不是那群打酱油,每天骂公司、摸鱼的那群人的凝聚力。
团建人太多了确实没太大的意义,更多就是完成公司的政治任务,对外宣传。我经常是参加团建的时候去认识公司的那些高级领导,和他们聊聊天,混个脸熟。后面我更多就带着小组的人一起出去浪,或者带着其他想和我们一起出去浪的同事出去浪,很多时候都是 AA 或者公司出一小部分,因为只要走公司账,他们经常玩不尽兴,总想着钱太少,玩的没意思,并且又有占便宜的心理,总之会玩的不舒服,所以很多时候我们都是自费出去玩。大家都是在外面打工的一群人,周末有很大一部分人想出去玩但是一个人不知道干啥,所以有一群人出去玩就会玩的比较好。
个人做法、看法,随意评价

# 第 19 天 (2019.10.16)

总览:

1、【html】 说说你对 html 中的置换元素和非置换元素的理解 ?

置换元素

置换元素是指:浏览器根据元素的标签和属性,来决定元素的具体显示内容。

例如:浏览器根据 <img > 标签的 src 属性显示图片。根据标签的 type 属性决定显示输入框还是按钮。

置换元素在其显示中生成了框,这也就是有的内联元素能够设置宽高的原因。

html 中的 <img><input><textarea><select><object > 都是置换元素,这些置换元素往往没有实际内容,即是一个空元素。

非置换元素

浏览器中的大多数元素都是不可置换元素,即其内容直接展示给浏览器。

例如 <label > 标签,<p > 标签里的内容会被浏览器直接显示给用户。

2、【css】 css 的属性 content 有什么作用呢?有哪些场景可以用到?

解析: CSS 属性 content 有什么作用呢?有哪些场景可以用到? MDN:content

CSS 的 content CSS 属性用于在元素的 ::before ::after 伪元素中插入内容。使用 content 属性插入的内容都是匿名的可替换元素。

场景:

  1. content: string value 字符串

    可以加入任何字符,包括 Unicode 编码等各种字符。

    <a class="demo" href="https://www.xunlei.com/" title="精彩,一下就有"
      >精彩,一下就有</a
    >
    
    .demo:after{ content: "↗" }
  2. 我们还可以通过 content 内字符串的变化,实现类似 加载中… 的动画效果

    .demo:after {
      animation: dot 1.6s linear both;
    }
    @keyframe dot {
      0% {
        content: ".";
      }
      33% {
        content: "..";
      }
      66% {
        content: "...";
      }
      100% {
        content: ".";
      }
    }

  1. content: uri value 外部资源,用于引用媒体文件,图片,图标,SVG 等。
.demo:after {
  content: url(https://img-vip-ssl.a.88cdn.com/img/xunleiadmin/5b9889e14dcdc.png);
}

3、【js】 "attribute" 和 "property" 有什么不同?

attribute 是我们在 html 代码中经常看到的键值对

<input id="the-input" type="text" value="Name:" />

上面代码中的 input 节点有三个 attribute:

  • id : the-input
  • type : text
  • value : Name:

property 是 attribute 对应的 DOM 节点的 对象属性 (Object field),

HTMLInputElement.id === "the-input";
HTMLInputElement.type === "text";
HTMLInputElement.value === "Name:";

区别:

<input id="the-input" type="typo" value="Name:" /> // 在页面加载后,
我们在这个input中输入 "Jack"

让我们来看看上面这个 input 节点的 attribute 和 property:

// attribute still remains the original value
input.getAttribute("id"); // the-input
input.getAttribute("type"); // typo
input.getAttribute("value"); // Name:

// property is a different story
input.id; // the-input
input.type; //  text
input.value; // Jack

可以看到,在 attribute 中,值仍然是 html 代码中的值。而在 property 中,type 被自动修正为了 text, 而 value 随着用户改变 input 的输入,也变更为了 Jack

这就是 attribute 和 Property 间的区别:

attribute 会始终保持 html 代码中的初始值,而 Property 是有可能变化的.

其实,我们从这两个单词的名称也能看出些端倪:

attribute 从语义上,更倾向于不可变更的

property 从语义上更倾向于在其生命周期中是可变的

Attribute or Property 可以自定义吗? :attribute 可以 property 不行

4、【软技能】最近都流行些什么?你经常会浏览哪些网站?

慕课网、掘金、github、stackoverflow/segmentfault、Google、相关技术官网文档

# 第 20 天 (2019.10.17)

总览:

1、【html】请描述 HTML 元素的显示优先级

解析: HTML 元素的显示优先级

帧元素 > HTML 元素优先,表单元素总 > 非表单元素优先
层级显示优先级: frameset > 表单元素 > 非表单元素

  • 表单元素包括:文本输入框,密码输入框,单选框,复选框,文本输入域,列表框等等;
  • 非表单元素包括:连接(a),div,table,span 等。

所有的 html 元素又可以根据其显示分成两类:有窗口元素以及无窗口元素。有窗口元素总是显示在无窗口元素的前面。
有窗口元素包括:select 元素,object 元素,以及 frames 元素等等。
无窗口元素:大部分 html 元素都是无窗口元素。

按照浏览器类型比较,HTML 元素的显示次序也有所不同:

2、【css】要让 Chrome 支持小于 12px 的文字怎么做?

解析:

Chrome 中有最小字号的限制,一般为 12px。原因是 Chrome 认为小于这个字号会影响阅读。

当需要小于 12px 字体的时候,有以下几个方法可以使用。

  • -webkit-text-size-adjust:none; 这个属性在高版本的 Chrome 中已经被废除。

  • 使用 transform: scale(0.5, 0.5) ,但使用 transform

    需要注意下面几点:

    • transform 对行内元素无效,因此要么使用 display: block; 要么使用 display: inline-block;
    • transform 即使进行了缩放,原来元素还是会占据对应的位置。因此需要做调整,最好是在外面再包一层元素,以免影响其他元素。
  • 作为图片。

最好的办法还是进行切图,或者就不要使用小于 12px 的字体。

3、【js】 写一个验证身份证号的方法

分析:身份证号码的组成:地址码 6 位 + 年份码 4 位 + 月份码 2 位 + 日期码 2 位 + 顺序码 3 位 + 校验码 1 位

解析:

  1. 粗暴型:只考虑位数、最后的 x \d {17}[\dXx]
  2. 一般型: /^\d {6}\d {4}(0 [1-9]|1 [0-2])(0 [1-9]|[12][0-9]|3 [01])\d {3}[\dXx]$/

4、【软技能】你会手写原生 js 代码吗?

解析:

其实是要看你理解原生的定义了。不管现在用的什么框架,我们很多写的业务代码不都是原生的嘛。还有很多公用的方法,一般用的是原生的 js。

# 第 21 天 (2019.10.18)

总览:

1、【html】 谈谈你对 input 元素中 readonly 和 disabled 属性的理解

解析:

  • 相同点:都会使文本框变成只读,不可编辑。
  • 不同点:
    1.disabled 属性在将 input 文本框变成只读不可编辑的同时,还会使文本框变灰,但是 readonly 不会。
    2.disabled 属性修饰后的文本框内容,在不可编辑的同时,通过 js 也是获取不到的。但是用 readonly 修饰后的文本框内容,是可以通过 js 获取到的,也就只是简单的不可编辑而已!
    3.disabled 属性对 input 文本框,单选 radio, 多选 checkbox 都适用,但是 readonly 就不适用,用它修饰后的单选以及多选按钮仍然是可以编辑状态的。
总结了前面老哥们的回答,再加上自己查了一下。

在表现上 readonly 和 disabled 都不能让用户对 input 进行编辑。但从含义上两者还是有较大的差别的。
readonly 直译为 “只读”,一般用于只允许用户填写一次的信息,提交过一次之后,就不允许再次修改了。

disabled 直译为 “禁用”,即这个 input 就是不允许填写和使用的(可能是因为权限或者其他原因)。
因此在外观上,readonly 与普通 input 无异,只是点击后无法进行编辑;而 disabled 的 input 呈灰色,也不允许点击。从这两点其实也可以看出,对于 input 的事件,readonly 会响应,而 disabled 是不响应的。并且在传输数据上,disabled 的数据是不会被获取和上传,readonly 的数据会被获取和上传。

2、【css】 说说你对 line-height 是如何理解的?

line-height 在日常用的最多的是让单行文字垂直居中(其实不需要设置 height ,一个 line-height 即可)。因为 line-height - font-size 为行距,一般会近似平分到文字的上下两边,使文字看上去垂直居中。如果需要多行文字的垂直居中,还需要加上 vertical-align: middle;

line-height 可以不设置单位,表示 font-size 的倍数。

另外对于非替换元素的纯内联元素,其高度是由 line-height 所决定的。

3【js】 写一个方法验证是否为中文

由于中文比较特殊,最稳妥的还是使用 unicode 来进行匹配。这两个 unicode 分别表示第一个和最后一个汉字。

function isChinese(str) {
  const re = /^[\u4e00-\u9fa5]+$/;
  return re.test(str);
}

4、【软技能】来说说你对重绘和重排的理解,以及如何优化?

重绘:

当盒子的位置、大小以及其他属性,例如颜色、字体大小等都确定下来之后,浏览器便把这些原色都按照各自的特性绘制一遍,将内容呈现在页面上。重绘是指一个元素外观的改变所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。
触发重绘的条件:改变元素外观属性。如:color,background-color,font-size 等。

重排 (回流):

当渲染树中的一部分 (或全部) 因为元素的规模尺寸,布局,隐藏等改变而需要重新构建,这就称为回流 (reflow)。每个页面至少需要一次回流,就是在页面第一次加载的时候。
重绘和重排的关系:在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。
所以,重排必定会引发重绘,但重绘不一定会引发重排
  触发重排的条件:任何页面布局和几何属性的改变都会触发重排,
比如:
   1、页面渲染初始化;(无法避免)
   2、添加或删除可见的 DOM 元素;
   3、元素位置的改变,或者使用动画;
   4、元素尺寸的改变 —— 大小,外边距,边框;
   5、浏览器窗口尺寸的变化(resize 事件发生时);
   6、填充内容的改变,比如文本的改变或图片大小改变而引起的计算值宽度和高度的改变;
触发重排的条件:改变元素的大小 位置 等如:width、height、pading、margin、position 等, 添加删除 DOM 操作等
重绘重排的代价:耗时,导致浏览器卡慢。

优化

1、浏览器自己的优化:浏览器会维护 1 个队列,把所有会引起回流、重绘的操作放入这个队列,等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会 flush 队列,进行一个批处理。这样就会让多次的回流、重绘变成一次回流重绘。
2、我们要注意的优化:我们要减少重绘和重排就是要减少对渲染树的操作,则我们可以合并多次的 DOM 和样式的修改。并减少对 style 样式的请求。
(1)直接改变元素的 className
(2)display:none;先设置元素为 display:none;然后进行页面布局等操作;设置完成后将元素设置为 display:block;这样的话就只引发两次重绘和重排;
(3)不要经常访问浏览器的 flush 队列属性;如果一定要访问,可以利用缓存。将访问的值存储起来,接下来使用就不会再引发回流;
(4)使用 cloneNode (true or false) 和 replaceChild 技术,引发一次回流和重绘;
(5)将需要多次重排的元素,position 属性设为 absolute 或 fixed,元素脱离了文档流,它的变化不会影响到其他元素;
(6)如果需要创建多个 DOM 节点,可以使用 DocumentFragment 创建完后一次性的加入 document;

# 第 22 天 (2019.10.20)

总览:

1、【html】 js 放在 html 的 <body > 和 <head > 有什么区别?

js 放在 <head> 中,如果不添加 async 或者 defer 时,当浏览器遇到 script 时,会阻塞 DOM 树的构建,进而影响页面的加载。当 js 文件较多时,页面白屏的时间也会变长。

在这个过程中,如果解析器遇到了一个脚本(script),它就会停下来,并且执行这个脚本,然后才会继续解析 HTML。如果遇到了一个引用外部资源的脚本(script),它就必须停下来等待这个脚本资源的下载,而这个行为会导致一个或者多个的网络往返,并且会延迟页面的首次渲染时间。

把 js 放到 <body> 里(一般在 </body> 的上面)时,由于 DOM 时顺序解析的,因此 js 不会阻塞 DOM 的解析。对于必须要在 DOM 解析前就要加载的 js,我们需要放在 <head> 中。

2、【css】说说浏览器解析 CSS 选择器的过程?

解析: 浏览器对于 CSS 选择器的解析过程是从右向左的。

.class ul li span {
  // css 属性
}

如果是这样的一个结构,浏览器会从右向左开始解析。因为一般来说,最右侧的节点范围反而会比较大,越向左限定的条件就越多。也因此 CSS 的选择器设计上不宜嵌套过多,会带来性能上的问题。

3、【js】你对 new 操作符的理解是什么?手动实现一个 new 方法

解析:

new 的理解

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象类型之一

new 步骤

模拟 new 操作前,要先知道 new 操作是发生了什么,就拿 new Object() 举例:

  1. 创建一个新对象
  2. 把新对象的原型指向构造函数的 prototype
  3. 把构造函数里的 this 指向新对象
  4. 返回这个新对象

构造函数:

先准备一个构造函数来 new 使用。

function constructorFunction(name, age) {
  this.name = name;
  this.age = age;
}
constructorFunction.prototype.say = function () {
  return "Hello " + this.name;
};

原生 new:

var obj = new constructorFunction("willian", 18);
console.log(obj.name, obj.age); //'willian', 18
console.log(obj.say()); //Hello willian

模拟 new

模拟的 new 暂称为 newNew (囡… 囡 哈哈~)
使用: newNew(constructor, arg1, arg2, ..) 第 0 个参数传入构造函数,1~n 个参数是构造函数的形参。
使用上面的构造函数试一下:

function newNew() {
  var newObj = {};
  // 1. 创建一个新对象
  var Con = [].shift.call(arguments);
  // 得到构造函数
  newObj.__proto__ = Con.prototype;
  // 2. 把新对象的原型指向构造函数的prototype
  var res = Con.apply(newObj, arguments);
  // 3. 把构造函数里的this指向新对象
  return typeof res === "object" ? res : newObj;
  // 4. 返回新对象
}
var obj = newNew(constructorFunction, "willian", 18);
console.log(obj.name, obj.age); //'willian', 18
console.log(obj.say()); //Hello willian

得到和 new 一样的答案,说明模拟成功。
你也可以 F12 打开控制台试一试。
以上参考:

  1. mqyqingfeng/Blog#13
  2. https://blog.csdn.net/liwenfei123/article/details/80580883

3、【软技能】 前端工程师这个职位你是怎么样理解的?聊聊它的前景?

广义的来说,只要涉及展示的都属于前端,包括各种系统,图片,动画,看得见就可以。从这个角度来说,前端永远不会被抛弃,会被淘汰的只有个体,因为个体是有极限,有局限的。
个人需要精确的定位,例如web工程师,也可以是电影特效工程师,工程师还分为软件硬件呢。

前景
具体到互联网行业的前端前景,在可见的范围内,前端承担的责任会增加而不是减少,保持进步就不会被淘汰,这点对于任何行业都一样,被抛弃的根本原因在于自身没有匹配需求的能力,而不是客观因素,那只是诱因,且必然发生。
话说本人是先从事一段时间后端才渐渐偏向前端的,正是因为看得见,便于分享的东西更吸引人。不是后端做不到,只是觉得旅途会更轻松一些
通过各种终端来向用户展示数据,或者给用户提供一些和后台的交互接口。
前景:首先,在我看来,一切和用户交互的终端都可以属于前端。并且随着现在跨端开发框架的兴起,比如Electron框架等,也使得前端的那套开发技术栈以及开发流程可以复制到桌面端来,使得前端的范畴越来越广泛。
并且,随着AR,VR技术的兴起,手机app中应用了大量的3维场景来提高用户体验,比如手机app上看房,看车,甚至是看一个城市的街景,都已经有了3D的场景,并且用户还能进行简单的操作。而这些都对前端提出了更高的要求

# 第 23 天(2019.10.21)

总览:

1、【html】 第 23 天关于 <form > 标签的 enctype 属性你有什么了解?

解析:

<form> 标签的 enctype 属性,用于控制表单上传的数据的编码格式。其值和 HTTP 请求的 Content-type 值相同。在数据提交到服务器之前,会以 enctype 值进行编码。

enctype 对应的值如下

用法
应用程序 /x-www-form-urlencoded 默认值,预设所有字符转进行编码(将空格转换为 “+” 符号,特殊字符转换为 ASCII HEX 值)
多部分 / 表单数据 不会对字符进行编码,当表单中有文件时必须要此编码
文字 / 纯文字 将空格转换为 “+” 符号,但不编码特殊字符

参考文章:
HTML form enctype 属性

2、【css】说说 CSS 的优先级是如何计算的?

解析: 点击此处

3、【js】 0.1 + 0.2、0.1 + 0.3 和 0.1 * 0.2 分别等于多少?并解释下为什么?

解析:

用一句话概括就是:

EcmaScrpt 规范定义 Number 的类型遵循了 IEEE754-2008 中的 64 位浮点数规则定义的小数后的有效位数至多为 52 位导致计算出现精度丢失问题!

这个问题也算是经常遇到的面试题之一了,楼上说的对,简单来说就是 js 中采用 IEEE754 的双精度标准,因为精度不足导致的问题,只是二进制表示 0.1 时这这样表示 1001100110011... (0011 无线循环),那么这些循环的数字被 js 裁剪后,就会出现精度丢失的问题,也就造成了 0.1 不再是 0.1 了 ,而是变成了 0.100000000000000002

我们可以来测试一下:

0.100000000000000002 === 0.1; //true

那么同样的,0.2 在二进制也是无限循环的,被裁剪后也失去了精度变成了 0.200000000000000002

0.200000000000000002 === 0.2; // true

由此我们可以得出:

0.1 + 0.2 === 0.30000000000000004; //true

所以自然 0.1+0.2!=0.3
那么如何解决这个问题;使用原生最简单的方法:

parseFloat((0.1 + 0.2).toFixed(10)) === 0.3; //true

参考:
深度剖析 0.1 +0.2 === 0.30000000000000004 的原因:https 😕/www.jianshu.com/p/d6b81e4e25e3

【软技能】 说说一件或几件(介绍下除了工作外)你觉得能为你面试加分的事

比如可以这么回答:

  1. 每年都要跑满 1000 公里,已经坚持 3 年了
  2. 我风雨无阻每天早上 4:30 起床坚持阅读,坚持了 10 年,已经习惯了!
  3. 我坚持每周至少三次去锻炼身体
  4. ……
    可以从坚持、勇敢、适应环境、担当、人际关系等个人性格特点方面回答。

# 第 24 天(2019.10.22)

总览:

1、【html】 说说你对属性 data - 的理解?

首先定义一下:data-是h5对自定义标签属性扩展的知识点,可以存储自定义属性,可以通过js获取到,一般会存储业务需要的数据,和vue中的bind很类似的
是暂存非用户输入的数据

2、【css】你有用过 CSS 预处理器吗?喜欢用哪个?原理是什么?

它能让你的 CSS 具备更加简洁、适应性更强、可读性更强、层级关系更加明显、更易于代码的维护等诸多好处。
CSS 预处理器种类繁多,目前 Sass、Less、用的比较多。
使用功能:
1、嵌套:反映层级和约束
2、变量和计算: 减少重复代码
3、Extend 和 Mixin 代码片段 (用的少)
4、循环:适用于复杂有规律的样式
5、import css 文件模块化
具体使用方法 均可百度

3、【js】如何快速让一个数组乱序,写出来

// 如何快速让一个数组乱序,写出来
var arr = [1, 2, 3, 4, 5];
arr.sort(() => (Math.random() > 0.5 ? 1 : -1));
console.log(arr); // 乱序

4、【软技能】 你经历过老板要求兼容 IE 吗?IE 几?有什么感悟

IE6,7一年,IE8半年,IE9一直以来的最低标准。
近半年PC项目直接Chrome,移动端项目直接-webkit-
总结就是最近没有兼容问题,爽。
感受就是兼容确实没啥大问题,你知道了IE的兼容问题之后尽量避开和熟练掌握对应的hack方法,其实也没有特别恐怖,怎么说呢,就是解决问题吧。
稳住,我们能赢!

# 第 25 天 (2019.10.23)

总览:

1、【html】 请说说 <script>、<script async > 和 <script defer > 的区别

单纯的 <script> 会阻塞 DOM 的渲染,如果放在 <head> 标签中,对页面的显示会有延迟。如果是用过 src 引入外部资源时,浏览器会先停止解析下载外部资源,之后再执行其中的 javaScript (即立即加载并渲染)。

在添加 async 或者 defer 之后, <script> 的下载不会阻塞 DOM 的渲染。两者的区别如下:

  • async 在脚本下载完成后立即执行(此时会阻塞 DOM 的渲染),并且多个 async 脚本存在时,执行的顺序取决于下载完成的顺序。因此对于有前后依赖关系的脚本(比如 jQuery 以及依赖 jQuery 的组件库,就不适合 async
  • defer 在的脚本执行放在 DOM 渲染之后(对于老的浏览器如果不支持 defer 就不行了)。并且多个脚本时,其执行顺序时按照引入顺序执行的。比较符合实际项目众多的需求,但为了兼容老版本浏览器,最佳的实践还是把 <script> 放在 </body> 前。

参考文章:
スクリプトの非同期読み込み (async, defer の違い)
defer 和 async 的区别

2、【css】 在页面中的应该使用奇数还是偶数的字体?为什么呢?

常用偶数号字体,但奇数号字体也没关系,例如 知乎正文使用15px字体,豆瓣电影使用13px字体
UI设计师导出的设计稿一般都是偶数号字体
偶数字号容易和页面其他标签的其他属性形成比例关系
Windows 自带的点阵宋体(中易宋体)从 Vista 开始只提供 12、14、16 px 这三个大小的点阵,
而 13、15、17 px 时用的是小一号的点阵(即每个字占的空间大了 1 px,但点阵没变),于是略显稀
疏。(没试过)

3、【js】 写一个判断设备来源的方法

根据 navigator.userAgent 来判断

function deviceType(){
       var ua = navigator.userAgent;
       var agent = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"];
       for(var i=0, i<agent.length; i++){
           if(ua.indexOf(agent[i])>0){
               alert(agent[i])
               break
           }
       }
   }

4、【软技能】说说你工作中遇到过比较难的技术问题是什么?是如何解决的?

这是在面试中经常被问到的一个问题,目的是查看面试者解决问题的能力。这里不做详细的某个技术难点来讲,因为可能你认为很难得问题,在别人那里根本不是事,就讲一下回答这个问题的思路吧。
这里的问题代表某个 bug 或某个难搞的需求。

回答思路:

  1. 问题出现的背景,比如说:‘在使用 Vue 开发 xxx 功能时中遇到 xxx…’
  2. 问题出现的原因在哪里,如果定位到的。比如:‘在使用 xx 调试发现的问题出现在 xx…’
  3. 查找问题解决方法,比如:‘在 xx 论坛看到解决方法,在某某交流群内提问,询问身边 (网上) 的技术大佬’
  4. 问题解决后达到了什么效果,比如:‘加载速度提升了约 4 倍,受到领导同事的一致好评…’
  5. 问题解决后有什么感悟或收获,比如:‘原来使用 xx 方法就能 xx,记录到我的 bug-log 中…’

# 第 26 天 (2019.10.24)

总览:

1、【html】 解释下你对 GBK 和 UTF-8 的理解?并说说页面上产生乱码的可能原因

gbk 和 utf8 的理解

我们这里将以最简单最容易理解的方式来描述 GBK 和 UTF8 的区别,以及它们分别是什么。

GBK 编码:是指中国的中文字符,其它它包含了简体中文与繁体中文字符,另外还有一种字符 “gb2312”,这种字符仅能存储简体中文字符。

UTF-8 编码:它是一种全国家通过的一种编码,如果你的网站涉及到多个国家的语言,那么建议你选择 UTF-8 编码。

GBK 和 UTF8 有什么区别?

UTF8 编码格式很强大,支持所有国家的语言,正是因为它的强大,才会导致它占用的空间大小要比 GBK 大,对于网站打开速度而言,也是有一定影响的。

GBK 编码格式,它的功能少,仅限于中文字符,当然它所占用的空间大小会随着它的功能而减少,打开网页的速度比较快。

2、【css】说说你对 z-index 的理解?

z-index 理解

当网页上出现多个由绝对定位(position:absolute)或固定定位(position:fixed)所产生的浮动层时,必然就会产生一个问题,就是当这些层的位置产生重合时,谁在谁的上面呢?或者说谁看得见、谁看不见呢?这时候就可以通过设置 z-index 的值来解决,这个值较大的就在上面,较小的在下面。

z-index 的意思就是在 z 轴的顺序,如果说网页是由 x 轴和 y 轴所决定的一个平面,那么 z 轴就是垂直于屏幕的一条虚拟坐标轴,浮动层就在这个坐标轴上,那么它们的顺序号就决定了谁上谁下了。

参考:

3、【js】 说说 bind、call、apply 的区别?并手写实现一个 bind 的方法

callapply 都是为了解决改变 this 的指向。作用都是相同的,只是传参的方式不同。

除了第一个参数外, call 可以接收一个参数列表, apply 只接受一个参数数组。 bind 绑定完之后返回一个新的函数,不执行。

Function.prototype.myCall = function (context = window) {
  context.fn = this;

  var args = [...arguments].slice(1);

  var result = context.fn(...args);
  // 执行完后干掉
  delete context.fn;
  return result;
};
Function.prototype.myApply = function (context = window) {
  context.fn = this;

  var result
  // 判断 arguments[1] 是不是 undefined
  if (arguments[1]) {
    result = context.fn(...arguments[1])
  } else {
    result = context.fn()
  }

  delete context.fn
  return result;
Function.prototype.myBind = function (context) {
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  var _this = this;
  var args = [...arguments].slice(1);
  // 返回一个函数
  return function F() {
    // 因为返回了一个函数,我们可以 new F(),所以需要判断
    if (this instanceof F) {
      return new _this(...args, ...arguments);
    }
    return _this.apply(context, args.concat(...arguments));
  };
};

4、【软技能】 你对 Git 的 branch 及工作流的理解是什么?

待续~

# 第 27 天 (2019.10.25)

总览:

1、【html】说说你对影子 (Shadow) DOM 的了解

影子节点 ShadowDOM

Shadow DOM 可以想象成我们在 Vue 或者 React 中使用的一个个组件,是一种将 HTML 结构、Style 封装起来的结构。我们熟悉的 <video> 标签,其实就是 Shadow DOM 的封装。

借用 MDN 上的图,可以看到 Shadow DOM 允许我们在 DOM 文档中插入一个 DOM 的子树。 Shadow Tree 会挂在 Shadow host 对应的 DOM 上。之后, Shadow DOM 与外层 DOM 不会相互影响,因此可以放心用来做组件。

具体的例子可以参考 MDN 给出的案例 ``

这个例子告诉我们可以利用 Shadow DOM 封装自己的 tag 标签,并且可以在网页中使用。

参考文章:
使用 shadow DOM
神奇的 Shadow DOM

2、【css】怎样修改 chrome 记住密码后自动填充表单的黄色背景?

设置表单属性 autocomplete=“off” 或者改变背景颜色为白色或透明

3、【js】说说你对 arguments 的理解,它是数组吗?

arguments 是一个对象。

js 不能像 java 一样实现重载, arguments 对象可以模拟重载。

js 中每个函数都会有 arguments 这个实例,它引用着函数的实参,可以用数组下标的方式 "[]" 引用 arguments 的元素。 arguments.length 为函数实参个数, arguments.callee 引用函数自身。

arguments 他的特性和使用方法

特性:

  1. arguments 对象和 Function 是分不开的。
  2. 因为 arguments 这个对象不能显式创建。
  3. arguments 对象只有函数开始时才可用。

使用方法:

虽然 arguments 对象并不是一个数组,但是访问单个参数的方式与访问数组元素的方式相同

例如:

arguments[0],arguments[1]…

arguments 不是数组,是类数组。
类数组 转 数组的方法有

[...arguments]
Array.from(arguments)
Array.prototype.slice.call(arguments)

# 第 28 天 (2019.10.26)

总览:

1、【html】 说说你对 <meta > 标签的理解

解析:关于 HTML 中 meta 标签的理解和总结

简介

这儿采用英文版 W3school 的解释:

The tag provides metadata about the HTML document. Metadata will not be displayed on the page, but will be machine parsable.

不难看出,其中的关键是 metadata,中文名叫元数据,是用于描述数据的数据。它不会显示在页面上,但是机器却可以识别。这么一来 meta 标签的作用方式就很好理解了。

用处

meta 常用于定义页面的说明,关键字,最后修改日期,和其它的元数据。这些元数据将服务于浏览器(如何布局或重载页面),搜索引擎和其它网络服务

组成

1、name 属性

name 属性主要用于描述网页,比如网页的关键词,叙述等。与之对应的属性值为 content,content 中的内容是对 name 填入类型的具体描述,便于搜索引擎抓取。
meta 标签中 name 属性语法格式是:

<meta name="参数" content="具体的描述" />

其中 name 属性共有以下几种参数。(A-C 为常用属性)

  • A. keywords (关键字)

    • 说明:用于告诉搜索引擎,你网页的关键字

    举例:

<meta name="keywords" content="Lxxyx,博客,文科生,前端" />
  • B. description (网站内容的描述)

    • 说明:用于告诉搜索引擎,你网站的主要内容。

    举例:

    <meta
      name="description"
      content="文科生,热爱前端与编程。目前大二,这是我的前端博客"
    />
  • C. viewport (移动端的窗口)

    • 说明:这个概念较为复杂,具体的会在下篇博文中讲述。
      这个属性常用于设计移动端网页。在用 bootstrap,AmazeUI 等框架时候都有用过 viewport。

    举例(常用范例):

    <meta name="viewport" content="width=device-width, initial-scale=1" />
  • D. robots (定义搜索引擎爬虫的索引方式)

    • 说明:robots 用来告诉爬虫哪些页面需要索引,哪些页面不需要索引。
      content 的参数有 all,none,index,noindex,follow,nofollow。默认是 all

    举例:

    <meta name="robots" content="none" />

    具体参数如下:

    1.none : 搜索引擎将忽略此网页,等价于 noindex,nofollow。
    2.noindex : 搜索引擎不索引此网页。
    3.nofollow: 搜索引擎不继续通过此网页的链接索引搜索其它的网页。
    4.all : 搜索引擎将索引此网页与继续通过此网页的链接索引,等价于 index,follow。
    5.index : 搜索引擎索引此网页。
    6.follow : 搜索引擎继续通过此网页的链接索引搜索其它的网页。

  • E. author (作者)

    • 说明:用于标注网页作者

    举例:

    <meta name="author" content="Lxxyx,841380530@qq.com" />
  • F. generator (网页制作软件)

    • 说明:用于标明网页是什么软件做的

    举例: (不知道能不能这样写):

    <meta name="generator" content="Sublime Text3" />
  • G. copyright (版权)

    • 说明:用于标注版权信息

    举例:

    <meta name="copyright" content="Lxxyx" /> //代表该网站为Lxxyx个人版权所有。
  • H. revisit-after (搜索引擎爬虫重访时间)

    • 说明:如果页面不是经常更新,为了减轻搜索引擎爬虫对服务器带来的压力,可以设置一个爬虫的重访时间。如果重访时间过短,爬虫将按它们定义的默认时间来访问。
      举例:
    <meta name="revisit-after" content="7 days" />
  • I. renderer (双核浏览器渲染方式)

    • 说明:renderer 是为双核浏览器准备的,用于指定双核浏览器默认以何种方式渲染页面。比如说 360 浏览器。
    <meta name="renderer" content="webkit" /> //默认webkit内核
    <meta name="renderer" content="ie-comp" /> //默认IE兼容模式
    <meta name="renderer" content="ie-stand" /> //默认IE标准模式

2、http-equiv 属性

这个我所认为的 http-equiv 意思的简介。
相当于HTTP的作用,比如说定义些HTTP参数啥的。

meta 标签中 http-equiv 属性语法格式是:

<meta http-equiv="参数" content="具体的描述" />

其中 http-equiv 属性主要有以下几种参数:

  • A. content-Type (设定网页字符集)(推荐使用 HTML5 的方式)

    • 说明:用于设定网页字符集,便于浏览器解析与渲染页面
    <meta http-equiv="content-Type" content="text/html;charset=utf-8" />
    //旧的HTML,不推荐
    
    <meta charset="utf-8" /> //HTML5设定网页字符集的方式,推荐使用UTF-8
  • B. X-UA-Compatible (浏览器采取何种版本渲染当前页面)

    • 说明:用于告知浏览器以何种版本来渲染页面。(一般都设置为最新模式,在各大框架中这个设置也很常见。)

    举例:

    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
    //指定IE和Chrome使用最新版本渲染当前页面
  • C. cache-control (指定请求和响应遵循的缓存机制)

用法 1.

说明:指导浏览器如何缓存某个响应以及缓存多长时间。这一段内容我在网上找了很久,但都没有找到满意的。
最后终于在 Google Developers 中发现了我想要的答案。

举例:

<meta http-equiv="cache-control" content="no-cache" />

共有以下几种用法:

  1. no-cache: 先发送请求,与服务器确认该资源是否被更改,如果未被更改,则使用缓存。
  2. no-store: 不允许缓存,每次都要去服务器上,下载完整的响应。(安全措施)
  3. public : 缓存所有响应,但并非必须。因为 max-age 也可以做到相同效果
  4. private : 只为单个用户缓存,因此不允许任何中继进行缓存。(比如说 CDN 就不允许缓存 private 的响应)
  5. maxage : 表示当前请求开始,该响应在多久内能被缓存和重用,而不去服务器重新请求。例如:max-age=60 表示响应可以再缓存和重用 60 秒。

参考链接:HTTP 缓存

用法 2.(禁止百度自动转码)

说明:用于禁止当前页面在移动端浏览时,被百度自动转码。虽然百度的本意是好的,但是转码效果很多时候却不尽人意。所以可以在 head 中加入例子中的那句话,就可以避免百度自动转码了。
举例:

<meta http-equiv="Cache-Control" content="no-siteapp" />
  • D. expires (网页到期时间)
    • 说明:用于设定网页的到期时间,过期后网页必须到服务器上重新传输。
      举例:
<meta http-equiv="expires" content="Sunday 26 October 2016 01:00 GMT" />
  • E. refresh (自动刷新并指向某页面)
    • 说明:网页将在设定的时间内,自动刷新并调向设定的网址。
      举例:
<meta http-equiv="refresh" content="2;URL=http://www.lxxyx.win/"> //意思是2秒后跳转向我的博客
  • F. Set-Cookie (cookie 设定)
    • 说明:如果网页过期。那么这个网页存在本地的 cookies 也会被自动删除。
<meta http-equiv="Set-Cookie" content="name, date"> //格式

<meta http-equiv="Set-Cookie" content="User=Lxxyx; path=/; expires=Sunday, 10-Jan-16 10:00:00 GMT"> //具体范例

2、【css】 rgba () 和 opacity 这两个的透明效果有什么区别呢?

1. opacity 是属性, rgba() 是函数,计算之后是个属性值; 2. opacity 作用于元素和元素的内容,内容会继承元素的透明度,取值 0-1; 3. rgba() 一般作为背景色 background-color 或者颜色 color 的属性值,透明度由其中的 alpha 值生效,取值 0-1;

扩展: 1. transparent 也是透明,是个属性值,颜色值,跟 #000 是一类,不过它是关键字来描述。 2. 如何隐藏一个元素?

回复@xiangshuo1992
隐藏元素可以从属性上进行隐藏,
display:none 通过定义自身的隐藏,并没有在页面存在dom节点,所以重新显示的时候,会导致页面重排。
visibility:hidden, 上面的不同,虽为隐藏,但在页面上还是有dom节点,个人认为比display:none较优。
opacity:1 透明度 给元素定义 隐藏、透明 是独立的透明属性,
transparent 透明颜色 是作为透明的颜色值使用,常见用在border隐藏做三角形,
rgba(0,0,0,1) 是颜色值的一种复合写法,既能显示颜色也能配合透明效果,
z-index=-1 定义层级属性若平常的页面显示为一,想看不到显示,可以把层级降低 用平常页面成为遮罩层,达到隐藏效果,换而言之,想突出一个元素也可把层级调大,类似于绝对定位的绝对效果。
, 还有一种是通过css3新增 用transform变化属性 rotate旋转 角度,也是可以达到隐藏效果,这里就涉及三维空间的思考。
欢迎大家,提出补充和有问题的地方。大家相互交流
@hbl045 visibility:hidden 视觉上隐藏了,但是DOM布局占位还在,所以有可能会影响现有的布局,应用场景并不多

隐藏元素也可以 transform: scale(0); 跟 visibility:hidden 一样,占位也是一直在的。
也可以设置宽高为零
还可以通过定位或者 translate 移出可视区域。

3、【js】解释下这段代码的意思!

[].forEach.call($$("*"), function (a) {
  a.style.outline =
    "1px solid #" + (~~(Math.random() * (1 << 24))).toString(16);
});

解析:

随机颜色获取:‘#’+(~~(Math.random () * (1 << 24))).toString (16)

作用

在你的 Chrome 浏览器的控制台中输入这段代码,你会发现不同 HTML 层都被使用不同的颜色添加了一个高亮的边框。是不是非常酷?但是,简单来说,这段代码只是首先获取了所有的页面元素,然后使用一个不同的颜色为它们添加了一个 1px 的边框。

解析

  • [].forEach.call() => 调用引用数组的 forEach 方法
  • $$('*') => document.querySelectorAll('*')
  • ~~a => parseInt(a)
  • 1<<24 => 对二进数 1 小数点右移 24 位
  • (parseInt(Math.random()*(1<<24)).toString(16)) => 获得了一个位于 0-16777216 之间的随机整数,也就是随机颜色,再使用 toString(16) 将它转化为十六进制数。
// $$('*') 为获取所有 dom 元素,返回数组
[].forEach.call($$("*"), function (a) {
  // forEach 的回调函数,这里的 a 是数组中每个 dom 元素,不是 a 标签
  a.style.outline =
    // ~~是取整 1<<24 是位运算 结果为 16777216
    // 之后的 toString(16) 为进行 16 进制的转换 即颜色
    "1px solid #" + (~~(Math.random() * (1 << 24))).toString(16);
});

因此这段代码的意思为,给页面所有 dom 元素添加随机颜色的边框。

4、【软技能】在浏览器中输入 url 到页面显示出来的过程发生了什么?

总体来说分为以下几个过程: 从输入 URL 到页面加载发生了什么

  1. DNS 解析
  2. TCP 连接
  3. 发送 HTTP 请求
  4. 服务器处理请求并返回 HTTP 报文
  5. 浏览器解析渲染页面
  6. 连接结束

# 第 29 天 (2019.10.27)

总览:

1、【html】你了解什么是无障碍 web(WAI)吗?在开发过程中要怎么做呢?

无障碍 web 是指能让视觉障碍的人也能根据屏幕阅读器的提示阅读网页。这一块只知道一个大致概念,国内使用较少(甚至还遇到过加了 title 被测试提 bug 的情况)

目前能想到的只有下面几点:

  • 尽可能地使用语义化标签,如 <section> , <article> 等标签
  • img 标签添加 alt
  • button 或者按钮上添加 title
  • 表单尽量使用 label for 可以和控件的 id 进行关联

参考文章:
无障碍 Web

2、【css】 请描述 css 的权重计算规则

权重值计算

选择器 案例 权重值
!important !important Infinity
内联样式 style="…" 1000
ID #id 100
class .class 10
属性 [type=‘text’] 10
伪类 :hover 10
标签 p 1
伪元素 ::first-line 1
相邻选择器、子代选择器、通配符 * > + 0

比较规则:

  • 1000>100。也就是说从左往右逐个等级比较,前一等级相等才往后比。
  • 在权重相同的情况下,后面的样式会覆盖掉前面的样式。
  • 继承属性没有权重值
  • 通配符、子选择器、相邻选择器等的。虽然权值为 0,但是也比继承的样式优先。
  • ie6 以上才支持 important ,并且尽量少用它。

3、【js】 写一个获取数组的最大值、最小值的方法

解析:

Array.prototype.max = function () {
  return Math.max.apply(null, this);
};

es6:

Math.max(...array);

4、【软技能】在工作中能让你最有成就感的是什么?并介绍下你最得意的作品吧

# 第 30 天 (2019.10.28)

总览:

1、【html】 网页上的验证码是为了解决什么问题?说说你了解的验证码种类有哪些

解决的问题:

  1. 防止机器行为,确定是人为操作,比如登陆、发帖等。
  2. 保护服务器,比如 12306 买票的时候,各种抢购的时候。

验证码的类型:

其实这种方式本质上是出于对系统的保护

  1. 滑动
  2. 手机验证码
  3. 图形验证码

2、【css】 描述下你所了解的图片格式及使用场景

通常网页在显示的图片(图形)的时候,有以下几种格式:GIF、PNG、JPG、SVG,还有个比较新的 WebP 格式。

▍GIF

优点:GIF 是动态的;支持无损耗压缩和透明度。

缺点:的详细的图片和写实摄影图像会丢失颜色信息;在大多数情况下,无损耗压缩效果不如 JPEG 格式或 PNG 格式;GIF 支持有限的透明度,没有半透明效果或褪色效果。

适用场景:主要用于比较小的动态图标。

▍PNG

优点:PNG 格式图片是无损压缩的图片,能在保证最不失真的情况下尽可能压缩图像文件的大小;图片质量高;色彩表现好;支持透明效果;提供锋利的线条和边缘,所以做出的 logo 等小图标效果会更好;更好地展示文字、颜色相近的图片。

缺点:占内存大,会导致网页加载速度慢;对于需要高保真的较复杂的图像,PNG 虽然能无损压缩,但图片文件较大,不适合应用在 Web 页面上。

适用场景:主要用于小图标或颜色简单对比强烈的小的背景图。

▍JPG

优点:占用内存小,网页加载速度快。

缺点:JPG 格式图片是有损压缩的图片,有损压缩会使原始图片数据质量下降,即 JPG 会在压缩图片时降低品质。

适用场景:由于这种格式图片对色彩表现比较好,所以适用于色彩丰富的图片。主要用于摄影作品或者大的背景图等。不合适文字比较多的图片。

▍SVG

优点:SVG 是矢量图形,不受像素影响,在不同平台上都表现良好;可以通过 JS 控制实现动画效果。

缺点:DOM 比正常的图形慢,而且如果其结点多而杂,就更慢;不能与 HTML 内容集成。

适用场景:主要用于设计模型的展示等。

▍WebP

优点:WebP 格式,谷歌(google)开发的一种旨在加快图片加载速度的图片格式。图片压缩体积大约只有 JPEG 的 2/3,并能节省大量的服务器宽带资源和数据空间。

缺点:相较编码 JPEG 文件,编码同样质量的 WebP 文件需要占用更多的计算资源。

适用场景:WebP 既支持有损压缩也支持无损压缩。将来可能是 JPEG 的代替品。

3、【js】写一个方法判断字符串是否为回文字符串

考点:正则表达式、数组 API

var isPalindrome = function (s) {
  if (s.length === 1) return true;
  const str = s.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
  const strReverse = str.split("").reverse().join("");
  return str === strReverse;
};

4、【软技能】 解释下 CRLF 是什么?

CRLF 是 carriagereturnlinefeed 的缩写。中文意思是回车换行。

# 第 31 天 (2019.10.29)

总览:

1、【html】 DOM 和 BOM 有什么区别?

BOM 是 Browser Object Model 的缩写,即浏览器对象模型。DOM 是 Document Object Model 的缩写,即文档对象模型。他们都是浏览器提供给 JavaScript 的 API 接口。

BOM 指 浏览器对象模型

提供了独立于内容而与浏览器窗口进行交互的对象。描述了与浏览器进行交互的方法和接口,可以对浏览器窗口进行访问和操作,譬如可以弹出新的窗口,改变状态栏中的文本。

DOM 指 文档对象模型

DOM 是针对 HTML 的基于树的 API。描述了处理网页内容的方法和接口,是 HTML 的 API,DOM 把整个页面规划成由节点层级构成的文档。

注意:只有 JS 的宿主环境是浏览器的时候才有 DOM 和 BOM ,在 Node 中是没有这两个对象的。

mark

2、【css】 让网页的字体变得清晰,变细用 CSS 怎么做?

解析:

  • 第一个反应是想到 font-weight: lighter; ,简单测试了下,是有效的,不过没有多平台测试。
  • 第二个想到的是 font-family 设置偏细的字体
  • 第三个是在重置样式里见过,针对 MAC,IOS 平台,有个 -webkit-webkit-font-smoothing: antialiased 样式。

3、【js】 写一个方法把 0 和 1 互转(0 置 1,1 置 0)

学学 js 里面中的特殊符号用法,了解写

定义 var a

  • !a && 1 || 0 ;
  • ~a+2
  • +!a
  • a === 1 ? 0 : 1 ( 三元表达式 )

4、【软技能】 对于有压力时,你是怎么抗压的?

现代人有点压力的正常的,我觉得抗压也是每一个成年人都要掌握的。
或者说排解压力比较准确吧,每个人都不一样,这里我就分享自己的解压方式吧。
解压方式:

  1. 听歌,压力大的时候在网易云上听会自己喜欢的歌。
  2. 运动,如果有时间就去运动吧,有时间就去打球、跑步,运动完之后一天的压力和疲惫都会减轻了很多。
  3. 找朋友倾诉,记住要找知心朋友,尽量不要找家人,不要让家人担心。

其实我觉得最重要的一点是:提高自己的能力,让那些对你有压力的事情变得简单,你自然就不会有压力的。

# 第 32 天 (2019.10.30)

总览:

1、【html】 说说你对 HTML 元素的显示优先级的理解

解析: HTML 元素的显示优先级 与 20 题一样

考点frameset, 优先级 元素html, 表单 面试HTML, 窗口

帧元素 > HTML 元素优先,表单元素总 > 非表单元素优先
层级显示优先级: frameset > 表单元素 > 非表单元素

  • 表单元素包括:文本输入框,密码输入框,单选框,复选框,文本输入域,列表框等等;
  • 非表单元素包括:连接(a),div,table,span 等。

所有的 html 元素又可以根据其显示分成两类:有窗口元素以及无窗口元素。有窗口元素总是显示在无窗口元素的前面。
有窗口元素包括:select 元素,object 元素,以及 frames 元素等等。
无窗口元素:大部分 html 元素都是无窗口元素。

按照浏览器类型比较,HTML 元素的显示次序也有所不同:

2、【css】说下 line-height 三种赋值方式有何区别?

<div class="parent1">
  <div class="child">line-height: 1.5em;</div>
</div>
<div class="parent2">
  <div class="child">line-height: 1.5;</div>
</div>

可以看到,当设置 line-height: 1.5em 时,很明显子 div 的文字已经超出自己的行高范围了,设置 line-height: 1.5 时子 div 的文字没有超出自己的行高。

这是由于 CSS 继承时的计算方式区别造成的,如示例,当我们给类名为 parent1 的父 div 设置 line-height:1.5em 时,该 div 的 font-size 为 14,此时经过计算父 div 的 line-height 为 14px*1.5=21px,然后子 div 的 line-height 就会继承 21px 这个值,而子 div 的 font-size 为 26px,自然会超出自己的行高范围。

而当我们给类名为 parent2 的父 div 设置 line-height:1.5 时,子 div 会直接继承 line-height:1.5 ,然后计算 26px*1.5=39px,不会超出自己的行高范围。

经过测试 line-height: 150%line-height: 1.5em 相同,都是先计算然后把固定的行高继承给子元素,所以我们可以总结一下,继承 line-height 的时候,带单位的先计算再继承,不带单位的直接继承

3、【js】 造成内存泄漏的操作有哪些?

解析: JS 哪些操作会造成内存泄漏?

现在的 GC 好像是越来越牛逼了,有时候感觉无效的闭包都能被回收掉(还没有做过测试)

  1. 意外的全局变量引起的内存泄漏
  2. 闭包引起的内存泄漏(主要是循环引用 ,其实和 闭包的关系不大)
  3. 没有清理的 DOM 元素
  4. 被遗忘的定时器或者回调
  5. 子元素存在引用引起的内存泄漏

# 第 33 天 (2019.10.30)

总览:

1、【html】html 和 html5 有什么区别呢?

  1. HTML5 简化了很多细微的语法,例如 doctype 的声明,只需要写 <!doctype html> 就行了。HTML5 与 HTML5,XHTML1 兼容,但是与 SGML 不兼容。
  2. 新增与语义化标签【header、footer、section、article 等】
  3. canvas 替代 Flash
html4一下是基于SGML(标准通用标记语言)的,H5不是,因为HTML要写很长的DTD规范描述,H5不用写
H5在HTML基础上增加了很多语义化的标签以及canvas和svg,媒体等的支持

2、【css】 用 CSS 绘制一个三角形

.triangle {
  width: 0;
  border-bottom: 35px solid lightgreen;
  border-left: 35px solid transparent;
}

3、【js】说说你对 this 的理解?

基本上可以归为四类,

  • 全局 this 是 window (默认指向)
  • 函数 this 是调用者 (隐式指向)
  • call 和 apply bind 的 this 第一个参数 (显示指向)
  • 构造函数的 this 是 new 之后的新对象 (构造器)

4、【软技能】 你对全栈工程师的理解是什么?

首先,我对于全栈工程师的要求很高。

  1. 独立完成页面
  2. 独立完成接口
  3. 超强学习能力

# 第 34 天 (2019.10.31)

总览:

1、【html】 Standards 模式和 Quirks 模式有什么区别?

解析:

后来查了下是浏览器渲染模式,最大区别还是盒模型的解释吧

标准盒模型:元素内容的宽度 = width ;元素的实际宽度 = width+ 2padding + 2border

怪异盒模型:元素内容宽度 = width - border 2 - paddin 2 ;实际宽度 = width

2、【css】浏览器是怎样判断元素是否和某个 CSS 选择器匹配?

先产生一个元素集合,然后从后往前判断;

浏览器先产生一个元素集合,这个集合往往由最后一个部分的索引产生(如果没有索引就是所有元素的集合)。然后向上匹配,如果不符合上一个部分,就把元素从集合中删除,直到真个选择器都匹配完,还在集合中的元素就匹配这个选择器了。

举个例子:

有选择器:
div.ready #wrapper > .bg-red
先把所有元素 class 中有 bg-red 的元素拿出来组成一个集合,然后上一层,对每一个集合中的元素,如果元素的 parent id 不为 #wrapper 则把元素从集合中删去。 再向上,从这个元素的父元素开始向上找,没有找到一个 tagNamedivclass 中有 ready 的元素,就把原来的元素从集合中删去。
至此这个选择器匹配结束,所有还在集合中的元素满足。大体就是这样,不过浏览器还会有一些奇怪的优化。
如图:

注意:

1、为什么从后往前匹配因为效率和文档流的解析方向。效率不必说,找元素的父亲和之前的兄弟比遍历所哟儿子快而且方便。关于文档流的解析方向,是因为现在的 CSS ,一个元素只要确定了这个元素在文档流之前出现过的所有元素,就能确定他的匹配情况;应用在即使 html 没有载入完成,浏览器也能根据已经载入的这一部分信息完全确定出现过的元素的属性。

2、为什么是用集合主要也还是效率。基于 CSS Rule 数量远远小于元素数量的假设和索引的运用,遍历每一条 CSS Rule 通过集合筛选,比遍历每一个元素再遍历每一条 Rule 匹配要快得多。

3、【js】 请用 canvas 写一个关于 520 浪漫表白的代码 ?

解析: 嘻嘻嘻~~老衲 阿弥陀佛

4、【软技能】 你了解什么是技术债务吗?

参考

# 第 35 天 (2019.11.01)

总览:

1、【html】 用一个 div 模拟 textarea 的实现

解析:

<style>
  .edit {
    width: 300px;
    height: 200px;
    padding: 5px;
    border: solid 1px #ccc;
    resize: both;
    overflow: auto;
  }
</style>
<div class="edit" contenteditable="true">
  这里是可以编辑的内容,配合容器的 overflow
  ,多行截断,自定义滚动条,简直好用的不要不要的。
</div>

2、【css】 使用 flex 实现三栏布局,两边固定,中间自适应

<style>
  .box {
    display: flex;
  }
  .left,
  .right {
    width: 100px;
    height: 100px;
    background: red;
    flex: 0 0 auto;
  }
  .middle {
    flex: 1 1 auto;
    width: 100%;
    height: 100px;
    background: salmon;
  }
</style>

<div class="box">
  <div class="left"></div>
  <div class="middle"></div>
  <div class="right"></div>
</div>

3、【js】 请你解释一个为什么 10.toFixed (10) 会报错?

之所以会报错,是因为在这里的 . 发生了歧义,它既可以理解为小数点,也可以理解为对方法的调用。
因为这个点紧跟于一个数字之后,按照规范,解释器就把它判断为一个小数点。

所以我们可以这样修改下:

(10).toFixed(10
10..toFixed(10)
10 .toFixed(10)
10.0.toFixed(10)

当然出现这个报错是因为前面这个数是整数,如果本来就是小数就不会出现这个报错。

4、【软技能】 谈一谈你知道的前端性能优化方案有哪些?

这个优化的范围挺大,但是总归可以分为 服务端优化客户端优化

整理如下

客户端优化

  • 减少 http 请求次数:CSS Sprites, JS、CSS 源码压缩、图片大小控制合适;网页 Gzip,CDN 托管,data 缓存 ,图片服务器。
  • 使用 CSS 雪碧图(CSS Sprites)CSS Sprites 一句话:将多个图片合并到一张单独的图片,这样就大大减少了页面中图片的 HTTP 请求。
  • 减少 DOM 操作次数,优化 javascript 性能。
  • 少用全局变量、减少 DOM 操作、缓存 DOM 节点查找的结果。减少 IO 读取操作。
  • 延迟加载 | 延迟渲染
  • 图片预加载,将样式表放在顶部,将脚本放在底部 加上时间戳。
  • 避免在页面的主体布局中使用 table,table 要等其中的内容完全下载之后才会显示出来,显示比 div+css 布局慢。

服务端优化

  • 尽量减少响应的体积,比如用 gzip 压缩,优化图片字节数,压缩 css 和 js;或加快文件读取速度,优化服务端的缓存策略。
  • 客户端优化 dom、css 和 js 的代码和加载顺序;或进行服务器端渲染,减轻客户端渲染的压力。
  • 优化网络路由,比如增加 CDN 缓存;或增加并发处理能力,比如服务端设置多个域名,客户端使用多个域名同时请求资源,增加并发量。

最后

对普通的网站有一个统一的思路,就是尽量向前端优化、减少数据库操作、减少磁盘 IO。向前端优化指的是,在不影响功能和体验的情况下,能在浏览器执行的不要在服务端执行,能在缓存服务器上直接返回的不要到应用服务器,程序能直接取得的结果不要到外部取得,本机内能取得的数据不要到远程取,内存能取到的不要到磁盘取,缓存中有的不要去数据库查询。
  减少数据库操作指减少更新次数、缓存结果减少查询次数、将数据库执行的操作尽可能的让你的程序完成(例如 join 查询),减少磁盘 IO 指尽量不使用文件系统作为缓存、减少读写文件次数等。程序优化永远要优化慢的部分,换语言是无法 “优化” 的。

涉及的知识点太多,从客户端浏览器、渲染机制、缓存、 网络请求、代码压缩合并、图片格式、服务器代理、数据库的查询…
暂时只能想到这么多,觉得自己答得并不是很好,希望有大佬回答一下这个问题。

缓存
http缓存 设置好cache-control expires Last-modified;
前端缓存 对于一些页面今天配置直接存储到localStorage中;对于长期不发生改变的代码可以直接通过server-work存储到本地;

优化加载
webpack 开启 tree-shaking 减少代码体积
通过preload prefetch优化加载资源的时间
import('').then()异步加载资源
图片小于30k的图片直接做成base64;
对于首屏的样式可以直接内嵌到html中;

服务端渲染
SSR
对于首页可以直接通过node jade模板引擎输出,其他页面继续使用前端渲染,优化首屏、SEO

# 第 36 天 (2019.11.02)

总览:

1、【html】 HTML 与 XHTML 二者有不同?

解析: HTML、XML、XHTML 有什么区别

定义:

  • HTML:HyperText Markup Language / 超文本标记语言
  • XML: Extensible Markup Language / 可扩展标记语言
  • XHTML: Extensible Hypertext Markup Language / 可扩展超文本标记语

作为一个前端,最熟悉是就是 HTML 了,所以我们先从 HTML 说起。

HTML 是用来描述和定义网页内容的标记语言,是构成网页的最基本的东西。
所谓超文本,就是说它除了能标记文本,还能标记其他的内容,比如:图片,链接,音频,视频等。
它的作用就是一个规范,告诉所有浏览器都统一标准,比如我给这段文字加个 <p> 标签,那就是告诉浏览器:这是一个段落。我加个 <img> 标签:这是一张图片,别弄错了。浏览器看到后,就会正确解析,产生相应的行为。

然后说一下 XML

它的表现形式就是给一个文档加一堆标签,说明每段文字是干什么的,有什么意义。这样做的目的是方便存储、传输、分享数据,人和机器都可以很方便的阅读。XML 和 HTML 有一个明显的区别就是:HTML 的标签都是预定义的,你不可以自己随便增加,比如你不能自造一个标签叫 <nihao> , 但是 XML 可以,你可以自己 “发明” 标签 ———— 这也是 “可扩展的” 一个含义。

HTML 和 XML 一结合,就产生了 XHTML

XHTML 就是以 XML 的语法形式来写 HTML.
XHTML 出现的原因是:HTML 是一种语法形式比较松散的标记语言,语法要求也不严格。比如大小可以混用,属性值随便你加不加引号,单引号还是双引号也随便你,标签也可以不闭合。HTML 标准的制定者 W3C 一看这样下去不行,所谓无规矩不成方圆,所以就把 XML 的语法形式往 HTML 上一套,出现了 XHTML,所以你也可以把 XHTML 理解为 HTML 的严格语法形式,除此之外,其它方面基本一样。
比如 XHTML 有一些强制的要求,如下:

  1. 必须包含一个文件头声明 <!DOCTYPE>
  2. 所有元素名必须小写
  3. 所有空元素必须关闭
  4. 所有属性名必须小写
  5. 所有属性值必须加引号
  6. 所有布尔值属性必须加上属性值

2、【css】 写出主流浏览器内核私有属性的 css 前缀

完善一下:现在用 scss 等预处理器用多了,前缀确实不怎么关注了。

-webkit- (谷歌,Safari, 新版 Opera 浏览器等)
-moz- (火狐浏览器)
-o- (旧版 Opera 浏览器等)
-ms- (IE 浏览器 和 Edge 浏览器)

3、【js】 请手写一个幻灯片(轮播)的效果

思路一 :元素并排浮动 改变 offset
思路二 :position 层叠 改变 z-Index

4、【软技能】 对于前端安全,你了解多少?说说你对 XSS 和 CSRF 的理解?

解析: 源地址

  • xss 输入 + 脚本
  • csrf 偷信息伪造请求

# 第 37 天 (2019.11.02)

总览:

1、【html】 html5 哪些标签可以优化 SEO?

解析:

meta 信息中的 title,description,keyword。尽量使用语义化的标签,不要都是 div

优化 SEO 应该是可以给爬虫有比较明确的含义的标签。尽可能地不要使用 div 到底。

  • meta: meta 标签中的 keywords 和 description
  • h1-h6
  • nav
  • section
  • article
  • footer
  • header

2、【css】不使用 border 画出 1px 高的线,在不同浏览器的标准和怪异模式下都能保持效果一样

解析:

<hr size="1" />
<div style="height: 1px; width: 100%;background: black"></div>

3、【js】 找到字符串中最长的单词,并返回它的长度

const str = "aaa b cc, hello word";
str.split(/\s|,/).reduce((acc, cur) => (acc > cur.length ? acc : cur.length));

4、【软技能】如果让你快速使用一门你不熟悉的新技术,你该怎么办?

我现在的做法:
1、一定先去官网查看官方文档和 API,其他别人写的教程无视。
2、下载官方 Demo 运行学习。
3、自己练习 1~2 个 Demo,涵盖常用的重要的 API 的使用,实践学习理解,有问题就谷歌。
4、运用到项目中。

# 第 38 天 (2019.11.04)

总览:

1、【html】说说你对 cookie 和 session 的理解?

解析:

由于 http 是无状态的,服务端没法记录客户端的状态。因此 cookie 和 session 本身就是为了记录客户端的状态。

只是 cookie 是存放在客户端而 session 是记录在服务端。cookie 可以在客户端生成也可以由服务器生成传给客户端,通过 name=value 的形式存储数据。

一般 cookie 会记录一个由服务端生成的 token,session 同样会记录这个 token。服务端就可以通过 token 来鉴别身份。
cookie: 可以通过客户端, 服务端设置, 容量小, 可以通过设置domain来实现同步登录, 除了name, value, 它还有多个选项, domain, path, secure, expires, 客户端和服务端可以通过cookie来通讯, 传递信息

session: 由服务端设置并发起, 是服务端对于用户行为的一种凭证, 通常也是由cookie来维持这种关系, 比如session_id, 或者现在webstorm设置的Webstorm-bb00fc34等! 通过这种维持两者的关系,
cookie: name=value 形式,可以设置过期时间,一般用来保持状态,不然每次都要登录

session:也是保存状态,在服务端产生,一些敏感信息放在服务端session,然后产生一个 sessionId,通过 cookie 传到客户端,然后每次客户端请求会带cookie,服务端从cookie中获取sessionID,从而获取敏感信息。不过浏览器一关就没了,不关过一会儿也会失效

把session放入cookie中便有了session cookie 2223

2、【css】 实现单行文本居中和多行文本左对齐并超出显示 "…"

解析: 有点懵~~~

.one {
  text-align: center
}

.multi {
  overflow: hidden
  text-overflow: ellipsis
  display: -webkit-box
  -webkit-line-clamp: 3
  -webkit-box-orient: vertical
}

3、【js】 说说你对 eval 的理解?

eval() 相当于一个小型的 js 解析器,接受一个字符串,可以把字符串解析成 js 代码并执行,所以有很有大的安全隐患,并且写进去的代码都是字符串,不利于维护,使用它执行代码性能也会大大折扣,所以正常情况下不建议使用

执行 js 代码,有性能问题,又可以执行一些恶意代码。webpack 中处理 soucemap 就用到了 eval,所有一个东西用途还是需要看场景。

# 第 39 天 (2019.11.08)

总览:

1、【html】 title 与 h1、b 与 strong、i 与 em 的区别分别是什么?

关于 titleh1title 是网页的标题。主要面向的对象是搜索引擎和通过搜索结果过来的人(面向外人,可以理解为报纸首页的标题)。而 h1 是网页内部的标题,是给已经进到页面的人看的(可以理解为报纸某个版面的大标题)。从人类的语境上来理解,两者并没有差别。

bstrong 的效果人眼上是无法区分的。在语义上, b 仅表示加粗既装饰用,我们应该使用 CSS 而不应该使用 b ;而 strong 则表示被包围的内容很重要,是语气上的感觉。对于搜索引擎来说,会把 bstrong 视为同一含义。因此我们在使用上需要注意。

iem 的区别类似 bstrong 的区别。 i 用于斜体展示,我们应该使用 CSS 而不应该使用 i ;而 em 则是对内容的强调,但程度没有 strong 那么高。同样,对搜索引擎来说,两者是没有区别的。

3、【js】 说说你对模块化的理解?

模块化解决了代码污染的问题。提高了代码的重复率以及让多人合作编程了可能。

模块化分为:

  • AMD: require.js 为代表,依赖前置,一律先加载再使用。
  • CMD: sea.js 为代表,依赖就近原则。
  • UMD: 同时支持 AMD 和 CMD 方法。
  • ES6 import/export

4、【软技能】 公钥加密和私钥加密是什么?

公钥加密,私钥解密吧。非对称加密的方式,比如 rsa 方式。对称加密 des

# 第 40 天 (2019.11.17)

总览:

1、【html】 html5 都有哪些新的特性?移除了哪些元素?

  1. 语义化的标签,header,footer,nav,section,article 等。
  2. 表单类型增多,date,datetime,email,range,url,time 等。
  3. 视频音频标签,localstorage,sessionstorage 等。canvas,拖动的 api。
  4. 移除了 basefont,big,center,font,s,strike,tt,u,frame,frameset,noframes;

2、【css】 怎么才能让图文不可复制?

MDN

.unselectable {
  -moz-user-select: none;
  -webkit-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

3、【js】 为什么会有跨域问题?怎么解决跨域?

跨域一句话的理解就是:服务端和请求端的地址不一样。

浏览器为了安全,产生了同源策略,协议、域名、端口有一个不同,就会产生跨域。跨域方式有 jsonp, 代理方式,cors,domain 改变主域相同,postmessage 也可以

什么是跨域

Ajax 的便利性大家都清楚,可以在不向服务器提交完整的页面的情况下,实现局部更新页面。但是浏览器处于对安全方面的考虑,不允许跨域调用其他页面的对象。
其实这个也不能怪浏览器,假设谁都可以随随便便向你发送请求,那样有很大的安全隐患。
根据浏览器的同源策略,只有当协议,域名,端口相同的时候才算是同源,反之则均视为是一个跨域的请求.
也就是说我刚刚的 Vue 端口是 8081 ,服务端端口是 8080 ,端口不一样,因为同源策略的存在 ,所有我的请求会失败。

怎么解决跨域 参考

  1. JSONP
  2. CORS
  3. Server Proxy

总结

常用的跨域方式基本就是这三种:

  1. JSONP
    优点是可以兼容老浏览器,缺点是只能发送 GET 请求
  2. CORS
    优点简单方便,支持 post 请求,缺点是需要后端的配合,不支持老版浏览器。。
  3. Server Proxy
    优点是前端正常发送 ajax 请求,缺点是后端会二次请求。

参考资料:

4、【软技能】说说你对 NodeJs 的理解及用途?

Node.js 是用来做什么的?

Node.js 是一个 javascript 运行环境。它让 javascript 可以开发后端程序,实现几乎其他后端语言实现的所有功能,可以与 PHP、Java、Python、.NET、Ruby 等后端语言平起平坐。

Nodejs 是基于 V8 引擎,V8 是 Google 发布的开源 JavaScript 引擎,本身就是用于 Chrome 浏览器的 js 解释部分,但是 Ryan Dahl 这哥们,鬼才般的,把这个 V8 搬到了服务器上,用于做服务器的软件。

优势:

  1. Nodejs 语法完全是 js 语法,只要你懂 js 基础就可以学会 Nodejs 后端开发
  2. NodeJs 超强的高并发能力
  3. 实现高性能服务器
  4. 开发周期短、开发成本低、学习成本低

Node.js 能干什么

# 第 41 天 (2019.11.17)

总览:

1、【html】 webSocket 怎么做兼容处理?

如何解决 WebSocket 的兼容性

2、【css】怎么让英文单词的首字母大写?

.demo {
  text-transform: capitalize;
}

/* Keyword values */
text-transform: capitalize;
text-transform: uppercase;
text-transform: lowercase;
text-transform: none;
text-transform: full-width;

capitalize
这个关键字强制每个单词的首字母转换为大写。

uppercase
这个关键字强制所有字符被转换为大写。

lowercase
这个关键字强制所有字符被转换为小写。

none
这个关键字阻止所有字符的大小写被转换。

full-width (实验性属性值)
这个关键字强制字符 — 主要是表意字符和拉丁文字 — 书写进一个方形里,并允许它们按照一般的东亚文字(比如中文或日文)对齐。

除了以上,还有一些基本上不会用到的默认值等,就不多说了。

/* Global values */
text-transform: inherit;
text-transform: initial;
text-transform: unset;

3、【js】说说你对 IIFE 的理解

最大的作用是创建一个独立的作用域

用 IIFE(匿名函数立即执行)实现,针对不需要复用的功能模块可以用 IIFE 完全消除全局变量,所以一般 IIFE 都是用来辅助命名空间 / 模块化方式的

# 第 42 天 (2019.11.18)

总览:

1、【html】解释下什么是 ISISO8859-2 字符集?

当今开发环境下,对于一个字符集,通常情况下,我们只需要有两个认识:

  • 它是 UTF-8 吗?
  • 如果不是,那它兼容 UTF-8 吗?

对于 ISO-8859,回答是:

它不是 UTF-8,但它兼容 UTF-8。它是 UTF-8 的子集。

当然,知道了也没用。你还是用 UTF-8。

注:你如果接手一个遗留项目,可能会接触到和 UTF-8 不同的其他字符集。你的开发体验通常会很差,因为其他工具都用 UTF-8。所以,最好的方法是:不要接手非 UTF-8 的遗留项目。


这个知识点在非科班的来看算是比较偏门的了。
查了一下才知道,原来是 Ascll 扩展部分的字符集。

ISO/IEC 8859-1,又称 Latin-1 或 “西欧语言”,ISO/IEC 8859-2 Latin-2 或 “中欧语言”,是国际标准化组织内 ISO/IEC 8859 的 8 位字符集。它以 ASCII 为基础,在空置的 0xA0-0xFF 的范围内,加入 192 个字母及符号,藉以供使用变音符号的拉丁字母语言使用。

我觉得可以把它看做是 Ascll 码的一部分

2、【css】 第 42 天 重置(初始化)css 的作用是什么?

我理解的,简单讲主要是为了 统一各个浏览器自带的默认样式而诞生的。

这是一个,还有就是 视觉问题 ,浏览器默认样式会影响我们的设计还原,而且默认样式一般不够美观,满足不了定制化的视觉需求,达不到视觉产品的信息传达目标。

3、【js】 window 对象和 document 对象有什么区别?

Window是浏览器的对象可以称为宿主对象。宿主对象包括(Bom,setTimeout,storage,work Server等 )
Document是文档对象,以html形式展示。是window对象里面的 一部分。

## window对象
   代表浏览器中的一个打开的窗口或者框架,window对象会在或者每次出现时被自动创建,在客户端JavaScript中,Window对象是全局对象global,所有的表达式都在当前的环境中计算,要引用当前的窗口不需要特殊的语法,可以把那个窗口属性作为全局变量使用,例如:可以只写document,而不必写window.document。同样可以把窗口的对象方法当做函数来使用,如:只写alert(),而不必写window.alert.
window对象实现了核心JavaScript所定义的全局属性和方法。

## document对象
   代表整个HTML文档,可以用来访问页面中的所有元素 。
每一个载入浏览器的HTML文档都会成为document对象。document对象使我们可以使用脚本(js)中对HTML页面中的所有元素进行访问。
document对象是window对象的一部分可以通过window.document属性对其进行访问
HTMLDocument接口进行了扩展,定义HTML专用的属性和方法,很多属性和方法都是HTMLCollection对象,其中保存了对锚、表单、链接以及其他可脚本元素的引用。

# 第 43 天 (2019.05.29)

总览:

1、【html】 如何让元素固定在页面底部?有哪些比较好的实践?

解析:这个是在结构的底部还是视图的底部 ,视图底部就是 fixed,结构的底部就是 sticky footer 布局咯~

2、【css】span 与 span 之间有看不见的空白间隔是什么原因引起的?有什么解决办法?

张鑫旭

  1. 方法之移除空格

    <div class="space">
        <a href="##">
        惆怅</a><a href="##">
        淡定</a><a href="##">
        热血</a>
    </div>
    
  2. 使用 margin 负值

  3. 让闭合标签吃胶囊

  4. 使用 font-size:0

  5. 使用 letter-spacing

  6. 使用 word-spacing

  7. 其他成品方法

3、【js】JQuery 的源码看过吗?能不能简单概括一下它的实现原理?

(function (window, undefined, document) {
  function jQuery(prop) {
    return new jQuery.prototype.init();
  }
  jQuery.prototype = {
    contructor: jQuery,
    init: function (prop) {},
    //  ...
  };
  jQuery.prototype.init.prototype = jQuery.prototype;
  window["jQuery"] = window["$"] = new jQuery();
})(window, undefined, document);

jQuery 是通过封装浏览器原生的 DOM API 实现 dom 元素的选取,然后封装到 jQuery 对象中去,同时根据浏览器检测对不同浏览器操作不同的 APi .jQuery 对象上高度集成了超的 API。当然 jQuery 还有做的更多比如,我们可以 new jQuery (‘div’), 也可以直接(div),这个巧妙地运算就是上面init方法;如果页面已经有('div'),这个巧妙地运算就是上面init方法;如果页面已经有时,jQuery 会先将接管把之前接管把之前的全局名保存下来 等后面使用是在释放、。。。大致了解。

4、【软技能】 最近在学什么?能谈谈你未来 3,5 年给自己的规划吗?

# 第 44 天 (2019.11.19)

总览:

1、【html】 说说 video 标签中预加载视频用到的属性是什么?

preload

属性 描述
autoplay autoplay 如果出现该属性,则视频在就绪后马上播放。
controls controls 如果出现该属性,则向用户显示控件,比如播放按钮。
height pixels 设置视频播放器的高度。
loop loop 如果出现该属性,则当媒介文件完成播放后再次开始播放。
preload preload 如果出现该属性,则视频在页面加载时进行加载,并预备播放。如果使用 “autoplay”,则忽略该属性。
src url 要播放的视频的 URL。
width pixels 设置视频播放器的宽度。

2、【css】手写一个满屏品字布局的方案?

flex 、float、grid 布局

3、【js】 深度克隆对象的方法有哪些,并把你认为最好的写出来

面试官:请你实现一个深克隆

1、前几年微博上流传着一个传说中最便捷实现深克隆的方法,JSON 对象 parse 方法可以将 JSON 字符串反序列化成 JS 对象,stringify 方法可以将 JS 对象序列化成 JSON 字符串,这两个方法结合起来就能产生一个便捷的深克隆.

const newObj = JSON.parse(JSON.stringify(oldObj));

缺点

  1. 它无法实现对函数 、RegExp 等特殊对象的克隆

  2. 会抛弃对象的 constructor, 所有的构造函数会指向 Object

  3. 对象有循环引用,会报错

  4. 构造一个深克隆函数

4、【软技能】说说你对 http、https、http2 的理解?

说说你对 http、https、http2.0 的理解?

# 第 45 天 (2019.11.23)

总览:

1、【html】 xml 与 html 有什么区别?

XML:
必须闭合;元素嵌套正确;标签小写; 必须有根元素

  1. html 不区分大小写,xml 区分大小写
  2. html 可以没有闭合标签,xml 必须有闭合标签
  3. html 可以拥有不带值的属性名,xml 中所有的属性必须带值
  4. html 是用于显示数据,xml 主要用于描述,存放数据
  5. XML 的多个空格不会被合并成一个空格,而 HTML 会。

2、【css】 你知道的等高布局有多少种?写出来

常用的多列等高布局

  1. 使用负 margin-bottom 和正 padding-bottom 对冲实现

    .Article > li {
      float: left;
      margin: 0 10px -9999px 0;
      padding-bottom: 9999px;
      background: #4577dc;
      width: 200px;
      color: #fff;
    }
  2. flex 布局

  3. 模仿 table 布局

  4. grid 布局

  5. js 计算

3、【js】 写出几种创建对象的方式,并说说他们的区别是什么?

const a = new Object(); // 创建, 不推荐  ---new 实例化
const b = {}; // 赋值, 性能比a要好  --字面量
const c = Object.create(); // 继承创建, Object.create(null) 很多框架都有用来做性能优化

new Object()

直接通过构造函数创建一个新对象。

var obj = new Object()
//等同于 var obj = {}

使用字面量的方式更简单,其实他俩是一样的。
优点是足够简单,缺点是每个对象都是独立的。

工厂模式

function createObj(name, age) {
  var obj = {};
  obj.name = name;
  obj.age = age;
  return obj;
}
var Anson = createObj("Anson", 18);
console.log(Anson);
//{name: "Anson", age: 18}

优点是 可以解决创建多个相似对象的问题,缺点是 无法识别对象的类型。

构造函数

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.sayName = function () {
    alert(this.name);
  };
}
var person = new Person("小明", 13);
console.log(person);
//Person {name: "小明", age: 13, sayName: ƒ}

优点是 可以创建特定类型的对象,缺点是 多个实例重复创建方法

(构造函数 + 原型)组合模式

function Person(name, age) {
  this.name = name;
  this.age = age;
  Person.prototype.sayName = function () {
    alert(this.name);
  };
}
var person = new Person("小白", 18);
console.log(person);
//Person {name: "小白", age: 18} __proto__ -> sayName: ƒ ()

优点 多个实例引用一个原型上的方法 比较常用

动态原型

function Person(name, age) {
  this.name = name;
  this.age = age;
  if (typeof this.sayName != "function") {
    Person.prototype.sayName = function () {
      alert(this.name);
    };
  }
}
var person = new Person("小红", 15);
console.log(person);
//Person {name: "小红", age: 15} 动态创建sayName: ƒ ()

优点 可以判断某个方法是否有效,来决定是否需要初始化原型,if 只会在仅在碰到第一个实例调用方法
时会执行,此后所有实例共享此方法,需要注意的一点是,不能重新原型对象。

寄生构造函数模式

function Person(name, age, job) {
  var o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  o.sayName = function () {
    console.log(this.name);
  };
  return o;
}
var friend = new Person("her", 18, "Front-end Engineer");
friend.sayName();
//her

除了使用 new 操作符,其他的和工厂函数一样,可以为对象创建构造函数。

稳妥模式

function Person(name, age){
    var o={};
    o.sayName=function(){ alert(name) }
    return o;
}
var person = ('小亮'24);
person.sayName();//’小亮‘

除了使用 person.sayName() 之外 ,没有办法在访问到 name 的值,适合在某些安全执行环景下使用。

Object.create()

const person = {
  isHuman: false,
  printIntroduction: function () {
    console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
  },
};

const me = Object.create(person);

me.name = "Matthew"; // "name" is a property set on "me", but not on "person"
me.isHuman = true; // inherited properties can be overwritten

me.printIntroduction();
// expected output: "My name is Matthew. Am I human? true"

传入一个原型对象,创建一个新对象,使用现有的对象来提供新创建的对象的 proto,实现继承。

参考:《JavaScript 高级程序设计第三版》、MDN

# 第 46 天 (2019.11.24)

总览:

1、【html】 页面中怎么嵌入 Flash?有哪些方法?写出来

从对象到 iframe - 其他嵌入技术

看一些能让您在网页中嵌入各种内容类型的元素: ](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/iframe), [ `` 元素。 <iframe> 用于嵌入其他网页,另外两个元素则允许您嵌入 PDF,SVG,甚至 Flash — 一种正在被淘汰的技术,但您仍然会时不时的看到它。

预备知识: 基本的计算机素养,安装基础软件文件处理 的基本知识,熟悉 HTML 基础知识(阅读 开始学习 HTML)以及本模块中以前的文章。
学习目标: 要了解如何使用 <object>、 ](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/embed)以及[ 在网页中嵌入部件,例如 Flash 电影或其他网页。

<embed > 和 <object > 元素

<embed > 和 <object > 元素的功能不同于 <iframe> —— 这些元素是用来嵌入多种类型的外部内容的通用嵌入工具,其中包括像 Java 小程序和 Flash,PDF(可在浏览器中显示为一个 PDF 插件)这样的插件技术,甚至像视频,SVG 和图像的内容!

注意插件是一种对浏览器原生无法读取的内容提供访问权限的软件。

页面中怎么嵌入 Flash?

  1. object + embed 传统的方法
  2. 单 object
  3. 双 object
  4. flex 提供的标准方法
  5. swfobject
  6. 单 embed 显示 ie7 和 ff3 下都能正常显示

2、【css】 说说你对媒体查询的理解?

为了适应不同的设备终端

3、【js】 写一个使两个整数进行交换的方法(不能使用临时变量)

解析:

  • ES5
var a = 1,
  b = 2;
a = b + a;
b = a - b;
a = a - b;
  • ES6
let [a, b] = [b, a];

异或取值

a ^= b;
b ^= a;
a ^= b;

# 第 47 天 (2019.12.05)

总览:

1、【html】HTML5 如何使用音频和视频?

HTML5 新标签可以直接用 video 和 audio,但是想要自动播放还有些兼容性问题,在手机上各浏览器需要做兼容处理。

2、【css】你是怎样抽离样式模块的?

样式模块?
通过组件化思想,用 BEM 方式命名。

3、【js】 请说说你对事件冒泡机制的理解?

按照 W3C 事件模型,事件流按照次序依次为 捕获阶段目标阶段冒泡阶段 。如果事件绑定时候,禁止了冒泡,则事件流会停止在目标阶段。

先说两个有关 DOM 事件流的概念 事件冒泡事件捕获

  • 事件冒泡: 事件沿着 DOM 树向上通知
  • 事件捕获:和事件冒泡相反,事件沿着 DOM 数向下通知

开发者可以自己决定事件处理注册到捕获阶段,或者是冒泡阶段。
element1.addEventListener('click',doSomething2,true) 如果最后一个参数为 true,则注册到捕获阶段。

事件委托 (事件代理)
介绍完上面的,事件委托是时候登场了。事件委托简单说起来就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。

# 第 48 天 (2019.12.05)

总览:

1、【html】 说说你对 WEB 标准和 W3C 的理解与认识?

web 标准

  • 什么是 web 标准:一系列标准的集合,包括结构化标准语言(html 等)、表现标准语言(css)、行为标准语言(EMCAScript 等)。这些标准大部分由万维网联盟起草和发布
  • 为什么使用 web 标准:为了解决因浏览器版本不同、软硬件设备不同导致的需多版本开发的问题
html是名词--表现
css是形容词--结构
javascript是动词--行为

以上这三个东西就形成了一个完整的网页,但是 js 改变时,可以会造成 css 和 html 的混乱,让这三个的界限不是那么清晰。

这个时候,web 标准就出来了,web 标准一般是将该三部分独立分开,使其更具有模块化。

W3C 对 web 标准提出了规范化的要求,也就是在实际编程中的一些代码规范:包含如下几点

1. 对于结构要求:(标签规范可以提高搜索引擎对页面的抓取效率,对 SEO 很有帮助)

标签字母要小写
标签要闭合
标签不允许随意嵌套 2. 对于 css 和 js 来说

尽量使用外链 css 样式表和 js 脚本。是结构、表现和行为分为三块,符合规范。同时提高页面渲染速度,提高用户的体验。
样式尽量少用行间样式表,使结构与表现分离,标签的 id 和 class 等属性命名要做到见文知义,标签越少,加载越快,用户体验提高,代码维护简单,便于改版

W3C:万维网联盟,是一个 web 开发的国际性联盟

2、【css】知道全屏滚动的原理是什么吗?它用到了 CSS 的哪些属性?

一、知识点

  • JS 滚动监听事件
  • JS 移动端 touch 监听事件
  • 函数节流
  • DOM 操作

二、代码分析

1.CSS

html, body 设置 overflow 为 hidden, 让视图中只包括一个分页;设置滑动分页的长宽都是 100%; 外部容器设置 transition 过渡效果,并设置为相对定位,滚动是修改外部容器的 Top 值,实现滚动效果.

html,
body {
  padding: 0;
  margin: 0;
  overflow: hidden;
}
.page-container {
  position: relative;
  top: 0;
  transition: all 1000ms ease;
  touch-action: none;
}
.page-item {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  border: 1px solid #ddd;
}

2.HTML

初始三个分页

<div class="page-container">
  <div class="page-item">1</div>
  <div class="page-item">2</div>
  <div class="page-item">3</div>
</div>

3.JavaScript

1. 初始化值
容器高度设置为窗口高度

var container = document.querySelector(".page-container");
// 获取根元素高度, 页面可视高度
var viewHeight = document.documentElement.clientHeight;
// 获取滚动的页数
var pageNum = document.querySelectorAll(".page-item").length;
// 初始化当前位置, 距离原始顶部距离
var currentPosition = 0;
// 设置页面高度
container.style.height = viewHeight + "px";

2. 初始化滚动事件
向下滚动时,当 currentPosition-整体分页高度 大的时候 (绝对值相比小的时候), 向下滚动;向上滚动时,当 currentPosition 大于 0 的时候,向上滚动.

// 向下滚动页面
function goDown() {
  if (currentPosition > -viewHeight * (pageNum - 1)) {
    currentPosition = currentPosition - viewHeight;
    container.style.top = currentPosition + "px";
  }
}

// 向上滚动页面
function goUp() {
  if (currentPosition < 0) {
    currentPosition = currentPosition + viewHeight;
    container.style.top = currentPosition + "px";
  }
}

3. 节流函数
即在规定时间内只会触发一次指定方法,用于滚动时防止多次触发

function throttle (fn, delay) {
  let baseTime = 0
  return function () {
    const currentTime = Date.now()
    if (baseTime + delay < currentTime) {
      fn.apply(this, arguments)
      baseTime = currentTime
    }
  }js
}

4. 监听鼠标滚动
滚动事件 firefox 与其他浏览器的事件不同,所以需要进行判断. deltaY 大于 0 的时候,想下滚动;反之,向上滚动.

var handlerWheel = throttle(scrollMove, 1000);
// https://developer.mozilla.org/en-US/docs/Web/API/Element/mousewheel_event#The_detail_property
// firefox的页面滚动事件其他浏览器不一样
if (navigator.userAgent.toLowerCase().indexOf("firefox") === -1) {
  document.addEventListener("mousewheel", handlerWheel);
} else {
  document.addEventListener("DOMMouseScroll", handlerWheel);
}
function scrollMove(e) {
  if (e.deltaY > 0) {
    goDown();
  } else {
    goUp();
  }
}

5. 监听移动端 touch 操作
当 touch 的最终位置大于起始位置时,则页面向上滚动;反之,向下滚动.

var touchStartY = 0;
document.addEventListener("touchstart", (event) => {
  touchStartY = event.touches[0].pageY;
});
var handleTouchEnd = throttle(touchEnd, 500);
document.addEventListener("touchend", handleTouchEnd);
function touchEnd(e) {
  var touchEndY = e.changedTouches[0].pageY;
  if (touchEndY - touchStartY < 0) {
    // 向上滑动, 页面向下滚动
    goDown();
  } else {
    goUp();
  }
}

# 第 49 天 (2019.12.15)

1【html】说说你对 target="_blank" 的理解?有啥安全性问题?如何防范?

通常在网页中使用链接时,你很可能会添加一个简单的 target="_blank" 属性到 a 标签上来让浏览器用一个新的标签页来打开一个 URL 地址。但是这一属性正在成为网络钓鱼者攻击的机会。 你从未注意的隐藏危险

2、【css】 假如设计稿使用了非标准的字体,你该如何去实现它?

解析:设计的职责是美观,前端的职责是尽可能还原,设计之所以会使用非标准的字体、甚至侵权的字体是因为不了解技术实现和版权意识。
所以先 沟通 ,告知设计实际的情况,然后在综合考量的情况下应该尽可能去实现,通常采用 载入字体图片化 的方式。

3、【js】写个还剩下多少天过年的倒计时

Math.floor((new Date("2019-12-31") - Date.now()) / (10 ** 5 * 36 * 24));

# 第 50 天 (2019.12.24)

2、【css】 列举 CSS 优化、提高性能的方法

加载性能

  1. 压缩 CSS
  2. 通过 link 方式加载,而不是 @import
  3. 复合属性其实分开写,执行效率更高,因为 CSS 最终也还是要去解析如 margin-left: left;

选择器性能

  1. 尽量少的使用嵌套,可以采用 BEM 的方式来解决命名冲突
  2. 尽量少甚至是不使用标签选择器,这个性能实在是差,同样的还有 * 选择器
  3. 利用继承,减少代码量

渲染性能

  1. 慎重使用高性能属性:浮动、定位;
  2. 尽量减少页面重排、重绘;
  3. css 雪碧图
  4. 自定义 web 字体,尽量少用
  5. 尽量减少使用昂贵属性,如 box-shadow/border-radius/filter/ 透明度 /:nth-child 等
  6. 使用 transform 来变换而不是宽高等会造成重绘的属性

暂且先这样吧,看来想回答好,得好好梳理下了。

木易杨每日一题

# 每日一题

每天一道大厂前端面试题,一年后再回头,会感谢曾经努力的自己!

待更新状态

今天 2019/12/23 ~ 💪

# 第 1 题 (2019/9/19)

题目:写 React / Vue 项目时为什么要在列表组件中写 key,其作用是什么?

公司:滴滴、饿了么

需了解 :vue 和 react 都是采用 diff 算法来对比新旧虚拟节点,从而更新节点。在 vue 的 diff 函数中。可以先了解一下 diff 算法。
在交叉对比的时候,当新节点跟旧节点 头尾交叉对比 没有结果的时候,会根据新节点的 key 去对比旧节点数组中的 key,从而找到相应旧节点(这里对应的是一个 key => index 的 map 映射)。如果没找到就认为是一个新增节点。而如果没有 key,那么就会采用一种遍历查找的方式去找到对应的旧节点。一种一个 map 映射,另一种是遍历查找。相比而言。map 映射的速度更快。

答案

key 是给每一个 vnode 的唯一 id, 可以 依靠key , 更 准确 ,更 的拿到 oldVnode 中对应的 vnode 节点。

key 的作用是为了在 diff 算法执行时更快的找到对应的节点,提高 diff 速度。

1、更准确

因为带 key 就不是 就地复用 了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。

2、更快

利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快。(这个观点,就是我最初的那个观点。从这个角度看,map 会比遍历更快。)

# 第 2 题(2019/9/20)

题目['1', '2', '3'].map(parseInt) what & why ?

答案 :第一眼看到这个题目的时候,脑海跳出的答案是 [1, 2, 3],但是 真正的答案是 [1, NaN, NaN]

console.log(parseInt("12")); //12
console.log(parseInt("08")); //8
console.log(parseInt("0x16")); //22
console.log(parseInt("-12")); //-12
console.log(parseInt("   -12")); //-12
console.log(parseInt("   -  12")); ///NAN
console.log(parseInt("124ref")); //124
console.log(parseInt("ref")); //NAN

以上几乎就是 parseInt 函数一个形式参数时的所有情况

radix 形参没指定的时候是 10,其次他是具有有效范围滴:[2, 36] 和特殊值 0
下面是英语渣渣的我翻译后,简化的 parseInt 执行步骤:(ECMAScript 原解析 -> 传送门

  1. 将第一个形参转换为字符串
  2. 识别 string 转换是否有 code unit,如果有 -> - 标记为负数, 0x0X 则把 radix 赋值为 16
  3. radix 形参(int 类型)是否存在,存在则重新赋值(会对实参进行 Int32 转化,无法转换成 int 类型则不会重新赋值 radix)
  4. radix 为 0,则设置 radix 为默认值 10
  5. 如果 radix 为 1,或者大于等于 37,parseInt 直接返回 NaN
  6. 如果 radix 为 [2, 36] 时则代表,string 参数分别是二进制,三进制(如果有得话~)… 三十六进制类型
  7. 然后对 string 进行的 radix 进制 -> 十进制转换

以上就是 parseInt 转换时的步骤,那么我们来开始解释 ['1', '2', '3'].map(parseInt)
at first, 答案是 [1, NaN, NaN]

(function () {
  var ret = ["1", "2", "3"].map((value, index) => {
    console.log(value, index);
    return parseInt(value, index);
  });
  console.log(ret);
})();

这是 ['1', '2', '3'].map(parseInt) 内部执行的剖析, valueindex 相信大家都懂,不懂请自行 MDN, 执行步骤为:
value='1',index=0 -> parseInt(value, index)
value='2',index=1 -> parseInt(value, index)
value='3',index=2 -> parseInt(value, index)
抽离出来,其实就是

parseInt("1", 0);
parseInt("2", 1);
parseInt("3", 2);

parseInt('3', 2) 这是根据二进制对字符串 3 进行十进制转换对吧!!!
exm??? 有毛病?没毛病,老铁,就是… 你家二进制有 3 ? 二进制不就是 01

因此返回 NaN

解决方案:

function returnInt(element) {
  return parseInt(element, 10);
}

["1", "2", "3"].map(returnInt); // [1, 2, 3]
// Actual result is an array of numbers (as expected)

// Same as above, but using the concise arrow function syntax
["1", "2", "3"].map((str) => parseInt(str));

// A simpler way to achieve the above, while avoiding the "gotcha":
["1", "2", "3"].map(Number); // [1, 2, 3]

// But unlike parseInt(), Number() will also return a float or (resolved) exponential notation:
["1.1", "2.2e2", "3e300"].map(Number); // [1.1, 220, 3e+300]
// For comparison, if we use parseInt() on the array above:
["1.1", "2.2e2", "3e300"].map((str) => parseInt(str)); // [1, 2, 3]

# 第 3 题(2019/9/21)

题目:什么是防抖和节流?有什么区别?如何实现?

公司 :挖财

解析

1、防抖

触发高频事件后 n 秒内函数只会执行一次,如果 n 秒内高频事件再次被触发,则重新计算时间

思路:

每次触发事件时都取消之前的延时调用方法

function debounce(fn, delay) {
  let timeout = null; // 创建一个标记用来存放定时器的返回值
  return function () {
    clearTimeout(timeout); // 每当用户输入的时候把前一个 setTimeout clear 掉
    timeout = setTimeout(() => {
      // 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数
      fn.apply(this, arguments);
    }, delay);
  };
}
function sayHi() {
  console.log("防抖成功");
}

var inp = document.getElementById("inp");
inp.addEventListener("input", debounce(sayHi, 500)); // 防抖

提出问题:请问,为什么要 fn.apply (this, arguments); 而不是这样 fn ()

解答:加上 apply 确保 在 sayHi 函数里的 this 指向的是 input 对象 (不然就指向 window 了,不是我们想要的)。
这里的箭头函数依旧是指向 input 对象。

mark

防抖应用场景:

  1. 搜索框输入查询,如果用户一直在输入中,没有必要不停地调用去请求服务端接口,等用户停止输入的时候,再调用,设置一个合适的时间间隔,有效减轻服务端压力。
  2. 表单验证
  3. 按钮提交事件。
  4. 浏览器窗口缩放,resize 事件等。

2、节流

高频事件触发,但在 n 秒内只会执行一次,所以节流会稀释函数的执行频率

思路:

每次触发事件时都判断当前是否有等待执行的延时函数

function throttle(fn, delay) {
  let canRun = true; // 通过闭包保存一个标记
  return function () {
    if (!canRun) return; // 在函数开头判断标记是否为true,不为true则return
    canRun = false; // 立即设置为false
    setTimeout(() => {
      // 将外部传入的函数的执行放在setTimeout中
      fn.apply(this, arguments);
      // 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。当定时器没有执行的时候标记永远是false,在开头被return掉
      canRun = true;
    }, 500);
  };
}
function sayHi(e) {
  console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener("resize", throttle(sayHi, 500));

mark

# 第 4 题 (2019/9/22)

题目 :介绍下 Set、Map、WeakSet 和 WeakMap 的区别?

解析: 木易杨前端进阶

Set
    成员唯一、无序且不重复
    [value, value],键值与键名是一致的(或者说只有键值,没有键名)
    可以遍历,方法有:add、delete、has

WeakSet
    成员都是对象
    成员都是弱引用,可以被垃圾回收机制回收,可以用来保存DOM节点,不容易造成内存泄漏
    不能遍历,方法有add、delete、has

Map
    本质上是键值对的集合,类似集合
    可以遍历,方法很多可以跟各种数据格式转换

WeakMap
    只接受对象作为键名(null除外),不接受其他类型的值作为键名
    键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的
    不能遍历,方法有get、set、has、delete

 Set与WeakSet区别:
    1. WeakSet只能存放对象
    2. WeakSet不支持遍历, 没有size熟悉
    3. WeakSet存放的对象不会计入到对象的引用技术, 因此不会影响GC的回收
    4. WeakSet存在的对象如果在外界消失了, 那么在WeakSet里面也会不存在

 Map与WeakMap区别
 	1. WeakMap只能接受对象作为键名字(null除外)
	2. WeakMap键名指向对象不会计入对象的引用数

# 第 5 题 (2019/9/22)

题目: 介绍下深度优先遍历和广度优先遍历,如何实现?

解析: 我先学习一下啥是 深度优先 和 广度优先… 惭愧!!!

# 第 6 题 (2019/9/23)

题目 :请分别用深度优先思想和广度优先思想实现一个拷贝函数?

弄懂了 优先遍历和广度优先遍历 再来做

# 第 7 题 (2019/9/23)

题目 :ES5/ES6 的继承除了写法以外还有什么区别?

解析更多

  1. class 声明会提升,但不会初始化赋值。 Foo 进入暂时性死区,类似于 letconst 声明变量。
  2. class 声明内部会启用严格模式。
  3. class 的所有方法(包括静态方法和实例方法)都是不可枚举的。
  4. class 的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有 [[construct]] ,不能使用 new 来调用。
  5. 必须使用 new 调用 class
  6. class 内部无法重写类名。

# 第 8 题(2019/9/24)

题目: setTimeout、Promise、Async/Await 的区别 ?

知识点 :这里涉及到 MicrotasksMacrotasks 、event loop 以及 JS 的异步运行机制。可参考

解析

我觉得这题主要是考察这三者在事件循环中的区别,事件循环中分为宏任务队列和微任务队列。
其中 setTimeout的回调函数放到宏任务队列里,等到执行栈清空以后执行;
promise.then里的回调函数会放到相应宏任务的微任务队列里,等宏任务里面的同步代码执行完再执行;async函数表示函数里面可能会有异步方法,await后面跟一个表达式,async方法执行时,遇到await会立即执行表达式,然后把表达式后面的代码放到微任务队列里,让出执行栈让同步代码先执行。

1、setTimeout

console.log("script start"); //1. 打印 script start
setTimeout(function () {
  console.log("settimeout"); // 4. 打印 settimeout
}); // 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数
console.log("script end"); //3. 打印 script start
// 输出顺序:script start->script end->settimeout

2、 Promise

Promise 本身是同步的立即执行函数, 当在 executor 中执行 resolve 或者 reject 的时候,此时是异步操作, 会先执行 then/catch 等,当主栈完成后,才会去调用 resolve/reject 中存放的方法执行,打印 p 的时候,是打印的返回结果,一个 Promise 实例。

console.log("script start");
let promise1 = new Promise(function (resolve) {
  console.log("promise1");
  resolve();
  console.log("promise1 end");
}).then(function () {
  console.log("promise2");
});
setTimeout(function () {
  console.log("settimeout");
});
console.log("script end");
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout

当 JS 主线程执行到 Promise 对象时,

  • promise1.then () 的回调就是一个 task
  • promise1 是 resolved 或 rejected: 那这个 task 就会放入当前事件循环回合的 microtask queue
  • promise1 是 pending: 这个 task 就会放入 事件循环的未来的某个 (可能下一个) 回合的 microtask queue 中
  • setTimeout 的回调也是个 task ,它会被放入 macrotask queue 即使是 0ms 的情况

3. async/await

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}
async function async2() {
  console.log("async2");
}

console.log("script start");
async1();
console.log("script end");

// 输出顺序:script start->async1 start->async2->script end->async1 end

async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。

举个例子:

async function func1() {
  return 1;
}

console.log(func1());

mark

很显然,func1 的运行结果其实就是一个 Promise 对象。因此我们也可以使用 then 来处理后续逻辑。

func1().then(res => {
    console.log(res);  // 30
}

await 的含义为等待,也就是 async 函数需要等待 await 后的函数执行完成并且有了返回结果(Promise 对象)之后,才能继续执行下面的代码。await 通过返回一个 Promise 对象来实现同步的效果。

更多可见 setTimeout、Promise、Async/Await

# 第 9 题(2019/9/25)

题目 : Async/Await 如何通过同步的方式实现异步?

公司 :头条、微医

解析 : [Async/Await 如何通过同步的方式实现异步](Async/Await 如何通过同步的方式实现异步)

首先, async/awaitGenerator 的语法糖

先来看一下二者的对比:

// Generator
run(function*() {
  const res1 = yield readFile(path.resolve(__dirname, '../data/a.json'), { encoding: 'utf8' });
  console.log(res1);
  const res2 = yield readFile(path.resolve(__dirname, '../data/b.json'), { encoding: 'utf8' });
  console.log(res2);
});

// async/await
const readFile = async ()=>{
  const res1 = await readFile(path.resolve(__dirname, '../data/a.json'), { encoding: 'utf8' });
  console.log(res1);
  const res2 = await readFile(path.resolve(__dirname, '../data/b.json'), { encoding: 'utf8' });
  console.log(res2);
  return 'done';
}
const res = readFile();

可以看到, async function 代替了 function*await 代替了 yield ,同时也无需自己手写一个自动执行器 run

现在再来看看 async/await 的特点:

  • await 后面跟的是 Promise 对象时,才会异步执行,其它类型的数据会同步执行
  • 执行 const res = readFile(); 返回的仍然是个 Promise 对象,上面代码中的 return 'done'; 会直接被下面 then 函数接收到
res.then(data => {
  console.log(data); // done
});

# 第 10 题(2019/9/26)

题目类型 :异步笔试题

公司 :头条

请写出下面代码的运行结果

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}
async function async2() {
  console.log("async2");
}
console.log("script start");
setTimeout(function () {
  console.log("setTimeout");
}, 0);
async1();
new Promise(function (resolve) {
  console.log("promise1");
  resolve();
}).then(function () {
  console.log("promise2");
});
console.log("script end");
/*  
    script start
    async1 start
    async2
    promise1
    script end
    async1 end
    promise2
    setTimeout
*/

# 第 11 题 (2019/9/26)

公司 :携程

已知如下数组:

var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10];

编写一个程序将数组扁平化去并除其中重复部分数据,最终得到一个升序且不重复的数组

Array.from(new Set(arr.flat(Infinity))).sort((a, b) => {
  return a - b;
}); //使用 flat
//或
Array.from(new Set(arr.toString().split(",")))
  .sort((a, b) => {
    return a - b;
  })
  .map(Number); //利用 toString()

# 第 12 题 (2019/9/27)

题目: JS 异步解决方案的发展历程以及优缺点。

公司 :滴滴、挖财、微医、海康

解析:

  1. 回调函数(callback)
setTimeout(() => {
  // callback 函数体
}, 1000);

缺点:回调地狱,不能用 try catch 捕获错误,不能 return

回调地狱的根本问题在于:

  • 缺乏顺序性: 回调地狱导致的调试困难,和大脑的思维方式不符
  • 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身,即(控制反转
  • 嵌套函数过多的多话,很难处理错误

优点 :解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。)

  1. Promise

Promise 就是为了解决 callback 的问题而产生的。

Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve () 包装

优点 :解决了回调地狱的问题

ajax("XXX1")
  .then((res) => {
    // 操作逻辑
    return ajax("XXX2");
  })
  .then((res) => {
    // 操作逻辑
    return ajax("XXX3");
  })
  .then((res) => {
    // 操作逻辑
  });

缺点 :无法取消 Promise ,错误需要通过回调函数来捕获

  1. Generato

特点 :可以控制函数的执行,可以配合 co 函数库使用

function* fetch() {
  yield ajax("XXX1", () => {});
  yield ajax("XXX2", () => {});
  yield ajax("XXX3", () => {});
}
let it = fetch();
let result1 = it.next();
let result2 = it.next();
let result3 = it.next();
  1. Async/await

async、await 是异步的终极解决方案

优点:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题

缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。

async function test() {
  // 以下代码没有依赖性的话,完全可以使用 Promise.all 的方式
  // 如果有依赖性的话,其实就是解决回调地狱的例子了
  await fetch("XXX1");
  await fetch("XXX2");
  await fetch("XXX3");
}

下面来看一个使用 await 的例子:

let a = 0;
let b = async () => {
  a = a + (await 10);
  console.log("2", a); // -> '2' 10
};
b();
a++;
console.log("1", a); // -> '1' 1

对于以上代码你可能会有疑惑,让我来解释下原因

  • 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为 await 内部实现了 generatorgenerator 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
  • 因为 await 是异步操作,后来的表达式不返回 Promise 的话,就会包装成 Promise.reslove(返回值) ,然后会去执行函数外的同步代码
  • 同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 0 + 10

上述解释中提到了 await 内部实现了 generator ,其实 await 就是 generator 加上 Promise 的语法糖,且内部实现了自动执行 generator 。如果你熟悉 co 的话,其实自己就可以实现这样的语法糖。

# 第 13 题 (2019/9/28)

题目 :Promise 构造函数是同步执行还是异步执行,那么 then 方法呢?

公司 :微医

看过 Event Loop 基础原理的就明白,Promise构造函数是同步执行,而 .then .catch .啥啥的是异步(还有process.nextTick等等,大家可以查),
而且放到了微队列中,async/await 中,await 前面的是同步,await 后面的是异步,写法上是这样,但是其实是 语法糖,最后还会转为 Promise.then的形式

.then()当然是同步执行,只不过是.then的cb被放入了微任务队列,产生了异步执行

promise是微观任务,setTimeout是宏观任务,先执行微观任务,在执行宏观任务;微观任务里,先执行同步再执行异步

# 第 14 题 (2019/9/29)

题目 :情人节福利题,如何实现一个 new ?

公司:兑吧

解析new 创建对象的过程发生了什么

// new 的作用
// 创建一个新对象obj
// 把obj的__proto__指向Dog.prototype 实现继承
// 执行构造函数,传递参数,改变this指向 Dog.call(obj, ...args)
// 最后把obj赋值给sanmao
var _new = function () {
  let constructor = Array.prototype.shift.call(arguments);
  let args = arguments;
  const obj = new Object();
  obj.__proto__ = constructor.prototype;
  constructor.call(obj, ...args);
  return obj;
};
var simao = _new(Dog, "simao");
simao.bark();
simao.sayName();
console.log(simao instanceof Dog); // true
// 这样写是不是简单点啊
function _new(fn, ...arg) {
  const obj = Object.create(fn.prototype);
  const ret = fn.apply(obj, arg);
  return ret instanceof Object ? ret : obj;
}

# 第 15 题 (2019/9/30)

题目 :简单讲解一下 http2 的多路复用

公司:网易

解析

在 HTTP/1 中,每次请求都会建立一次HTTP连接,也就是我们常说的3次握手4次挥手,这个过程在一次请求过程中占用了相当长的时间,即使开启了 Keep-Alive ,解决了多次连接的问题,但是依然有两个效率上的问题:

    第一个:串行的文件传输。当请求a文件时,b文件只能等待,等待a连接到服务器、服务器处理文件、服务器返回文件,这三个步骤。我们假设这三步用时都是1秒,那么a文件用时为3秒,b文件传输完成用时为6秒,依此类推。(注:此项计算有一个前提条件,就是浏览器和服务器是单通道传输)

    第二个:连接数过多。我们假设Apache设置了最大并发数为300,因为浏览器限制,浏览器发起的最大请求数为6,也就是服务器能承载的最高并发为50,当第51个人访问时,就需要等待前面某个请求处理完成。

HTTP/2的多路复用就是为了解决上述的两个性能问题。
在 HTTP/2 中,有两个非常重要的概念,分别是帧(frame)和流(stream)。
帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流,流也就是多个帧组成的数据流。
多路复用,就是在一个 TCP 连接中可以存在多条流。换句话说,也就是可以发送多个请求,对端可以通过帧中的标识知道属于哪个请求。通过这个技术,可以避免 HTTP 旧版本中的队头阻塞问题,极大的提高传输性能。

# 第 16 题 (2019/10/01)

题目 :谈谈你对 TCP 三次握手和四次挥手的理解

解析关于三次握手与四次挥手面试官想考我们什么?— 不看后悔系列

三次握手:

  1. 第一次握手:客户端给服务器发送一个 SYN 报文。
  2. 第二次握手:服务器收到 SYN 报文之后,会应答一个 SYN+ACK 报文。
  3. 第三次握手:客户端收到 SYN+ACK 报文之后,会回应一个 ACK 报文。
  4. 服务器收到 ACK 报文之后,三次握手建立完成。

这里我顺便解释一下为啥只有三次握手才能确认双方的接受与发送能力是否正常,而两次却不可以
第一次握手:客户端发送网络包,服务端收到了。这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
第二次握手:服务端发包,客户端收到了。这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。
第三次握手:客户端发包,服务端收到了。这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。

四次挥手:

  1. 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。
  2. 第二次握手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 + 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。
  3. 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。
  4. 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 + 1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态
  5. 服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。

这里特别需要主要的就是 TIME_WAIT 这个状态了,这个是面试的高频考点,就是要理解,为什么客户端发送 ACK 之后不直接关闭,而是要等一阵子才关闭。这其中的原因就是,要确保服务器是否已经收到了我们的 ACK 报文,如果没有收到的话,服务器会重新发 FIN 报文给客户端,客户端再次收到 ACK 报文之后,就知道之前的 ACK 报文丢失了,然后再次发送 ACK 报文。

至于 TIME_WAIT 持续的时间至少是一个报文的来回时间。一般会设置一个计时,如果过了这个计时没有再次收到 FIN 报文,则代表对方成功就是 ACK 报文,此时处于 CLOSED 状态。

# 第 17 题 (2019/10/08)

题目 : A、B 机器正常连接后,B 机器突然重启,问 A 此时处于 TCP 什么状态 ?

如果 A 与 B 建立了正常连接后,从未相互发过数据,这个时候 B 突然机器重启,问 A 此时处于 TCP 什么状态?如何消除服务器程序中的这个状态?(超纲题,了解即可)

# 第 18 题 (2019/10/08)

题目 :React 中 setState 什么时候是同步的,什么时候是异步的?

公司 :微医 React 中 setState 真的是异步的吗

1.在组件生命周期中或者react事件绑定中,setState是通过异步更新的。
2.在延时的回调或者原生事件绑定的回调中调用setState不一定是异步的。
这个结果并不说明setState异步执行的说法是错误的,更加准确的说法应该是setState不能保证同步执行。
Dan Abramov也多次提到今后会将setState彻底改造为异步的,从js conf中提到的suspend新特新也印证了这一点。
这里所说的同步异步,并非真正的同步异步,通常是同步执行的。
这里的异步指的是多个状态会合成到一起进行批量更新。

# 第 19 题 (2019/10/08)

题目 :React setState 笔试题,下面的代码输出什么?

class Example extends React.Component {
  constructor() {
    super();
    this.state = {
      val: 0,
    };
  }

  componentDidMount() {
    this.setState({ val: this.state.val + 1 });
    console.log(this.state.val); // 第 1 次 log

    this.setState({ val: this.state.val + 1 });
    console.log(this.state.val); // 第 2 次 log

    setTimeout(() => {
      this.setState({ val: this.state.val + 1 });
      console.log(this.state.val); // 第 3 次 log

      this.setState({ val: this.state.val + 1 });
      console.log(this.state.val); // 第 4 次 log
    }, 0);
  }

  render() {
    return null;
  }
}
1、第一次和第二次都是在 react 自身生命周期内,触发时 isBatchingUpdates 为 true,所以并不会直接执行更新 state,而是加入了 dirtyComponents,所以打印时获取的都是更新前的状态 02、两次 setState 时,获取到 this.state.val 都是 0,所以执行时都是将 0 设置成 1,在 react 内部会被合并掉,只执行一次。设置完成后 state.val 值为 13、setTimeout 中的代码,触发时 isBatchingUpdates 为 false,所以能够直接进行更新,所以连着输出 23//输出: 0 0 2 3

# 第 20 题 (2019/10/09)

题目 :介绍下 npm 模块安装机制,为什么输入 npm install 就可以自动安装对应的模块?

解析

1、npm 模块安装机制:

  • 发出 npm install 命令
  • 查询 node_modules 目录之中是否已经存在指定模块
    • 若存在,不再重新安装
    • 若不存在
      • npm 向 registry 查询模块压缩包的网址
      • 下载压缩包,存放在根目录下的 .npm 目录里
      • 解压压缩包到当前项目的 node_modules 目录

2、npm 实现原理

输入 npm install 命令并敲下回车后,会经历如下几个阶段(以 npm 5.5.1 为例):

  1. 执行工程自身 preinstall

当前 npm 工程如果定义了 preinstall 钩子此时会被执行。

  1. 确定首层依赖模块

首先需要做的是确定工程中的首层依赖,也就是 dependencies 和 devDependencies 属性中直接指定的模块(假设此时没有添加 npm install 参数)。

工程本身是整棵依赖树的根节点,每个首层依赖模块都是根节点下面的一棵子树,npm 会开启多进程从每个首层依赖模块开始逐步寻找更深层级的节点。

  1. 获取模块

获取模块是一个递归的过程,分为以下几步:

  • 获取模块信息。在下载一个模块之前,首先要确定其版本,这是因为 package.json 中往往是 semantic version(semver,语义化版本)。此时如果版本描述文件(npm-shrinkwrap.json 或 package-lock.json)中有该模块信息直接拿即可,如果没有则从仓库获取。如 packaeg.json 中某个包的版本是 ^1.1.0,npm 就会去仓库中获取符合 1.x.x 形式的最新版本。
  • 获取模块实体。上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址检查本地缓存,缓存中有就直接拿,如果没有则从仓库下载。
  • 查找该模块依赖,如果有依赖则回到第 1 步,如果没有则停止。
  1. 模块扁平化(dedupe)

上一步获取到的是一棵完整的依赖树,其中可能包含大量重复模块。比如 A 模块依赖于 loadsh,B 模块同样依赖于 lodash。在 npm3 以前会严格按照依赖树的结构进行安装,因此会造成模块冗余。

从 npm3 开始默认加入了一个 dedupe 的过程。它会遍历所有节点,逐个将模块放在根节点下面,也就是 node-modules 的第一层。当发现有重复模块时,则将其丢弃。

这里需要对重复模块进行一个定义,它指的是模块名相同semver 兼容。每个 semver 都对应一段版本允许范围,如果两个模块的版本允许范围存在交集,那么就可以得到一个兼容版本,而不必版本号完全一致,这可以使更多冗余模块在 dedupe 过程中被去掉。

比如 node-modules 下 foo 模块依赖 lodash@^1.0.0,bar 模块依赖 lodash@^1.1.0,则 ^1.1.0 为兼容版本。

而当 foo 依赖 lodash@^2.0.0,bar 依赖 lodash@^1.1.0,则依据 semver 的规则,二者不存在兼容版本。会将一个版本放在 node_modules 中,另一个仍保留在依赖树里。

举个例子,假设一个依赖树原本是这样:

node_modules
– foo
---- lodash@version1

– bar
---- lodash@version2

假设 version1 和 version2 是兼容版本,则经过 dedupe 会成为下面的形式:

node_modules
– foo

– bar

– lodash(保留的版本为兼容版本)

假设 version1 和 version2 为非兼容版本,则后面的版本保留在依赖树中:

node_modules
– foo
– lodash@version1

– bar
---- lodash@version2

  1. 安装模块

这一步将会更新工程中的 node_modules,并执行模块中的生命周期函数(按照 preinstall、install、postinstall 的顺序)。

  1. 执行工程自身生命周期

当前 npm 工程如果定义了钩子此时会被执行(按照 install、postinstall、prepublish、prepare 的顺序)。

最后一步是生成或更新版本描述文件,npm install 过程完成。

参考 npm 模块安装机制简介

详解 npm 的模块安装机制

npm install 的实现原理

# 第 21 题 (2019/10/10)

题目: 有以下 3 个判断数组的方法,请分别介绍它们之间的区别和优劣

Object.prototype.toString.call () 、 instanceof 以及 Array.isArray ()

解析:

1、Object.prototype.toString.call()

每一个继承 Object 的对象都有 toString 方法,如果 toString 方法没有重写的话,会返回 [Object type] ,其中 type 为对象的类型。但当除了 Object 类型的对象外,其他类型直接使用 toString 方法时,会直接返回都是内容的字符串,所以我们需要使用 call 或者 apply 方法来改变 toString 方法的执行上下文。

const an = ["Hello", "An"];
an.toString(); // "Hello,An"
Object.prototype.toString.call(an); // "[object Array]"

这种方法对于所有基本的数据类型都能进行判断,即使是 null 和 undefined 。

Object.prototype.toString.call("An"); // "[object String]"
Object.prototype.toString.call(1); // "[object Number]"
Object.prototype.toString.call(Symbol(1)); // "[object Symbol]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(function () {}); // "[object Function]"
Object.prototype.toString.call({ name: "An" }); // "[object Object]"

Object.prototype.toString.call() 常用于判断浏览器内置对象时。

更多实现可见 谈谈 Object.prototype.toString

2、instanceof

instanceof 的内部机制是通过判断对象的原型链中是不是能找到类型的 prototype

使用 instanceof 判断一个对象是否为数组, instanceof 会判断这个对象的原型链上是否会找到对应的 Array 的原型,找到返回 true ,否则返回 false

[] instanceof Array; // true

instanceof 只能用来判断对象类型,原始类型不可以。并且所有对象类型 instanceof Object 都是 true。

[] instanceof Object; // true

3、Array.isArray()

  • 功能:用来判断对象是否为数组

  • instanceof 与 isArray

    当检测 Array 实例时, Array.isArray 优于 instanceof ,因为 Array.isArray 可以检测出 iframes

    var iframe = document.createElement("iframe");
    document.body.appendChild(iframe);
    xArray = window.frames[window.frames.length - 1].Array;
    var arr = new xArray(1, 2, 3); // [1,2,3]
    
    // Correctly checking for Array
    Array.isArray(arr); // true
    Object.prototype.toString.call(arr); // true
    // Considered harmful, because doesn't work though iframes
    arr instanceof Array; // false
  • Array.isArray()Object.prototype.toString.call()

    Array.isArray() 是 ES5 新增的方法,当不存在 Array.isArray() ,可以用 Object.prototype.toString.call() 实现。

    if (!Array.isArray) {
      Array.isArray = function(arg) {
        return Object.prototype.toString.call(arg) === '[object Array]';
      };
    }
    

# 第 22 题 (2019/10/11)

题目: 介绍下重绘和回流(Repaint & Reflow),以及如何进行优化?

解析: 浏览器的回流与重绘 (Reflow & Repaint)

1、浏览器渲染机制

  • 浏览器采用流式布局模型( Flow Based Layout
  • 浏览器会把 HTML 解析成 DOM ,把 CSS 解析成 CSSOMDOMCSSOM 合并就产生了渲染树( Render Tree )。
  • 有了 RenderTree ,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上。
  • 由于浏览器使用流式布局,对 Render Tree 的计算通常只需要遍历一次就可以完成,但 table 及其内部元素除外,他们可能需要多次计算,通常要花 3 倍于同等元素的时间,这也是为什么要避免使用 table 布局的原因之一

2、重绘

由于节点的几何属性发生改变或者由于样式发生改变而不会影响布局的,称为重绘,例如 outline , visibility , colorbackground-color 等,重绘的代价是高昂的,因为浏览器必须验证 DOM 树上其他节点元素的可见性。

3、回流

回流是布局或者几何属性需要改变就称为回流。回流是影响浏览器性能的关键因素,因为其变化涉及到部分页面(或是整个页面)的布局更新。一个元素的回流可能会导致了其所有子元素以及 DOM 中紧随其后的节点、祖先节点元素的随后的回流。

<body>
  <div class="error">
    <h4>我的组件</h4>
    <p><strong>错误:</strong>错误的描述…</p>
    <h5>错误纠正</h5>
    <ol>
      <li>第一步</li>
      <li>第二步</li>
    </ol>
  </div>
</body>

在上面的 HTML 片段中,对该段落 ( <p> 标签) 回流将会引发强烈的回流,因为它是一个子节点。这也导致了祖先的回流( div.errorbody – 视浏览器而定)。此外, <h5><ol> 也会有简单的回流,因为其在 DOM 中在回流元素之后。大部分的回流将导致页面的重新渲染。

回流必定会发生重绘,重绘不一定会引发回流。

4、浏览器优化

现代浏览器大多都是通过队列机制来批量更新布局,浏览器会把修改操作放在队列中,至少一个浏览器刷新(即 16.6ms)才会清空队列,但当你获取布局信息的时候,队列中可能有会影响这些属性或方法返回值的操作,即使没有,浏览器也会强制清空队列,触发回流与重绘来确保返回正确的值

主要包括以下属性或方法:

  • offsetTopoffsetLeftoffsetWidthoffsetHeight
  • scrollTopscrollLeftscrollWidthscrollHeight
  • clientTopclientLeftclientWidthclientHeight
  • widthheight
  • getComputedStyle()
  • getBoundingClientRect()

所以,我们应该避免频繁的使用上述的属性,他们都会强制渲染刷新队列 **。**

5、减少重绘与回流

  1. CSS

    • 使用 transform 替代 top

    • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局

    • 避免使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局。

    • 尽可能在 DOM 树的最末端改变 class,回流是不可避免的,但可以减少其影响。尽可能在 DOM 树的最末端改变 class,可以限制了回流的范围,使其影响尽可能少的节点。

    • 避免设置多层内联样式,CSS 选择符从右往左匹配查找,避免节点层级过多。

      <div>
        <a> <span></span> </a>
      </div>
      <style>
        span {
          color: red;
        }
        div > a > span {
          color: red;
        }
      </style>

      对于第一种设置样式的方式来说,浏览器只需要找到页面中所有的 span 标签然后设置颜色,但是对于第二种设置样式的方式来说,浏览器首先需要找到所有的 span 标签,然后找到 span 标签上的 a 标签,最后再去找到 div 标签,然后给符合这种条件的 span 标签设置颜色,这样的递归过程就很复杂。所以我们应该尽可能的避免写过于具体的 CSS 选择器,然后对于 HTML 来说也尽量少的添加无意义标签,保证层级扁平

    • 将动画效果应用到 position 属性为 absolute 或 fixed 的元素上,避免影响其他元素的布局,这样只是一个重绘,而不是回流,同时,控制动画速度可以选择 requestAnimationFrame ,详见探讨 requestAnimationFrame

    • 避免使用 CSS 表达式,可能会引发回流。

    • 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点,例如 will-changevideoiframe 等标签,浏览器会自动将该节点变为图层。

    • CSS3 硬件加速(GPU 加速),使用 css3 硬件加速,可以让 transformopacityfilters 这些动画不会引起回流重绘 。但是对于动画的其它属性,比如 background-color 这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

  2. JavaScript

    • 避免频繁操作样式,最好一次性重写 style 属性,或者将样式列表定义为 class 并一次性更改 class 属性。
    • 避免频繁操作 DOM,创建一个 documentFragment ,在它上面应用所有 DOM操作 ,最后再把它添加到文档中。
    • 避免频繁读取会引发回流 / 重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
    • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

详见浏览器的重绘与回流(Repaint、Reflow)

# 第 23 题 (2019/10/12)

题目: 介绍下观察者模式和订阅 - 发布模式的区别,各自适用于什么场景

解析:

我们先来看下这两个模式的实现结构:

观察者模式: 观察者(Observer)直接订阅(Subscribe)主题(Subject),而当主题被激活的时候,会触发(Fire Event)观察者里的事件。

发布订阅模式: 订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Topic),当发布者(Publisher)发布该事件(Publish topic)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。

区别:

观察者模式和发布订阅模式最大的区别就是发布订阅模式有个事件调度中心。

观察者模式由具体目标调度,每个被订阅的目标里面都需要有对观察者的处理,这种处理方式比较直接粗暴,但是会造成代码的冗余。

而发布订阅模式中统一由调度中心进行处理,订阅者和发布者互不干扰,消除了发布者和订阅者之间的依赖。这样一方面实现了解耦,还有就是可以实现更细粒度的一些控制。比如发布者发布了很多消息,但是不想所有的订阅者都接收到,就可以在调度中心做一些处理,类似于权限控制之类的。还可以做一些节流操作。

观察者模式是不是发布订阅模式

网上关于这个问题的回答,出现了两极分化,有认为发布订阅模式就是观察者模式的,也有认为观察者模式和发布订阅模式是真不一样的。

其实我不知道发布订阅模式是不是观察者模式,就像我不知道辨别模式的关键是设计意图还是设计结构(理念),虽然《JavaScript 设计模式与开发实践》一书中说了分辨模式的关键是意图而不是结构。

如果以结构来分辨模式,发布订阅模式相比观察者模式多了一个中间件订阅器,所以发布订阅模式是不同于观察者模式的;如果以意图来分辨模式,他们都是实现了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新,那么他们就是同一种模式,发布订阅模式是在观察者模式的基础上做的优化升级。

不过,不管他们是不是同一个设计模式,他们的实现方式确实有差别,我们在使用的时候应该根据场景来判断选择哪个。

# 第 24 题 (2019/10/13)

题目: 聊聊 Redux 和 Vuex 的设计思想

解析: 关于 Flux,Vuex,Redux 的思考 Flux 架构入门教程

Flux 是一种前端状态管理架构思想,专门解决软件的结构问题。
基于 Flux 的设计思想,出现了一批前端状态管理框架。
他们给出了一些库用于实现 Flux 的思想,并在 Flux 的基础上做了一些改进。
在这些框架里,当前最热门的莫过于 Redux 和 Vuex 了

Flux

Flux 数据流的顺序是:

View 发起 Action->Action 传递到 Dispatcher->Dispatcher 将通知 Store->Store 的状态改变通知 View 进行改变

Redux

Redux 相对于 Flux 的改进:

  • 把 store 和 Dispatcher 合并,结构更加简单清晰
  • 新增 state 角色,代表每个时间点 store 对应的值,对状态的管理更加明确

Redux 数据流的顺序是:

View 调用 store.dispatch 发起 Action->store 接受 Action (action 传入 reducer 函数,reducer 函数返回一个新的 state)-> 通知 store.subscribe 订阅的重新渲染函数

ps: 阮一峰老师的 Redux+React 小 demo

Vuex

Vuex 是专门为 Vue 设计的状态管理框架,
同样基于 Flux 架构,并吸收了 Redux 的优点

Vuex 相对于 Redux 的不同点有:

  • 改进了 Redux 中的 Action 和 Reducer 函数,以 mutations 变化函数取代 Reducer,
    无需 switch, 只需在对应的 mutation 函数里改变 state 值即可
  • 由于 Vue 自动重新渲染的特性,无需订阅重新渲染函数,只要生成新的 State 即可

Vuex 数据流的顺序是:

View 调用 store.commit 提交对应的请求到 Store 中对应的 mutation 函数 ->store 改变 (vue 检测到数据变化自动渲染)

# 第 25 题 (2019/10/14)

题目: 说说浏览器和 Node 事件循环的区别

解析:

浏览器

关于微任务和宏任务在浏览器的执行顺序是这样的:

  • 执行一只 task(宏任务)
  • 执行完 micro-task 队列 (微任务)

如此循环往复下去

浏览器的 task(宏任务)执行顺序在 html#event-loops 里面有讲就不翻译了
常见的 task(宏任务) 比如:setTimeout、setInterval、script(整体代码)、 I/O 操作、UI 渲染等。
常见的 micro-task 比如: new Promise ().then (回调)、MutationObserver (html5 新特性) 等。

Node

Node 的事件循环是 libuv 实现的,引用一张官网的图:

大体的 task(宏任务)执行顺序是这样的:

  • timers 定时器:本阶段执行已经安排的 setTimeout () 和 setInterval () 的回调函数。
  • pending callbacks 待定回调:执行延迟到下一个循环迭代的 I/O 回调。
  • idle, prepare:仅系统内部使用。
  • poll 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,它们由计时器和 setImmediate () 排定的之外),其余情况 node 将在此处阻塞。
  • check 检测:setImmediate () 回调函数在这里执行。
  • close callbacks 关闭的回调函数:一些准备关闭的回调函数,如:socket.on (‘close’, …)。

微任务和宏任务在 Node 的执行顺序

Node 10 以前:

  • 执行完一个阶段的所有任务
  • 执行完 nextTick 队列里面的内容
  • 然后执行完微任务队列的内容

Node 11 以后:
和浏览器的行为统一了,都是每执行一个宏任务就执行完微任务队列

# 第 26 题 (2019/10/16)

题目: 介绍模块化发展历程

可从 IIFE、AMD、CMD、CommonJS、UMD、webpack (require.ensure)、ES Module、 <script type="module"> 这几个角度考虑。

解析: es6,amd,smd,commonjs 思维导图

模块化主要是用来抽离公共代码,隔离作用域,避免变量冲突等。

IIFE: 使用自执行函数来编写模块化,特点:在一个单独的函数作用域中执行代码,避免变量冲突

(function () {
  return {
    data: [],
  };
})();

AMD: 使用 requireJS 来编写模块化,特点:依赖必须提前声明好

define("./index.js", function (code) {
  // code 就是index.js 返回的内容
});

CMD: 使用 seaJS 来编写模块化,特点:支持动态引入依赖文件

define(function (require, exports, module) {
  var indexCode = require("./index.js");
});

CommonJS: nodejs 中自带的模块化。

var fs = require("fs");

UMD:兼容 AMD,CommonJS 模块化语法。

webpack(require.ensure):webpack 2.x 版本中的代码分割。

ES Modules: ES6 引入的模块化,支持 import 来引入另一个 js 。

import a from "a";

# 第 27 题 (2019/10/16)

题目: 全局作用域中,用 const 和 let 声明的变量不在 window 上,那到底在哪里?如何去获取?。

解析:

在 ES5 中,顶层对象的属性和全局变量是等价的,var 命令和 function 命令声明的全局变量,自然也是顶层对象。

var a = 12;
function f() {}

console.log(window.a); // 12
console.log(window.f); // f(){}

但 ES6 规定,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性,但 let 命令、const 命令、class 命令声明的全局变量,不属于顶层对象的属性。

let aa = 1;
const bb = 2;

console.log(window.aa); // undefined
console.log(window.bb); // undefined

在函数的内部属性 [[Scopes]] 中找到了

如上图,在全局作用域中用 const 声明的变量在函数 noop 中可以正常访问,没有问题。我用 dir 方法打印出函数 noop 的属性,最后在 [[Scopes]] 属性内找到了消失的全局变量 abcd

# 第 28 题 (2019/10/17)

题目: cookie 和 token 都存放在 header 中,为什么不会劫持 token?

解析:

  1. 首先 token 不是防止 XSS 的,而是为了防止 CSRF 的;
  2. CSRF 攻击的原因是浏览器会自动带上 cookie,而浏览器不会自动带上 token

cookie:登陆后后端生成一个 sessionid 放在 cookie 中返回给客户端,并且服务端一直记录着这个 sessionid,客户端以后每次请求都会带上这个 sessionid,服务端通过这个 sessionid 来验证身份之类的操作。所以别人拿到了 cookie 拿到了 sessionid 后,就可以完全替代你。

token:登陆后后端不返回一个 token 给客户端,客户端将这个 token 存储起来,然后每次客户端请求都需要开发者手动将 token 放在 header 中带过去,服务端每次只需要对这个 token 进行验证就能使用 token 中的信息来进行下一步操作了。

xss:用户通过各种方式将恶意代码注入到其他用户的页面中。就可以通过脚本获取信息,发起请求,之类的操作。

csrf:跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。csrf 并不能够拿到用户的任何信息,它只是欺骗用户浏览器,让其以用户的名义进行操作。

csrf 例子:假如一家银行用以运行转账操作的 URL 地址如下: http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName
那么,一个恶意攻击者可以在另一个网站上放置如下代码: <img src="<http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman>">
如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 资金。

上面的两种攻击方式,如果被 xss 攻击了,不管是 token 还是 cookie,都能被拿到,所以对于 xss 攻击来说,cookie 和 token 没有什么区别。但是对于 csrf 来说就有区别了。

以上面的 csrf 攻击为例:

  • cookie:用户点击了链接,cookie 未失效,导致发起请求后后端以为是用户正常操作,于是进行扣款操作。
  • token:用户点击链接,由于浏览器不会自动带上 token,所以即使发了请求,后端的 token 验证不会通过,所以不会进行扣款操作。

这是个人理解的为什么只劫持 cookie 不劫持 token 的原因。

# 第 29 题 (2019/10/17)

题目: 聊聊 Vue 的双向数据绑定,Model 如何改变 View,View 又是如何改变 Model 的

解析:

VM 主要做了两件微小的事情:

  • 从 M 到 V 的映射(Data Binding),这样可以大量节省你人肉来 update View 的代码(将数据绑定到 view)
  • 从 V 到 M 的事件监听(DOM Listeners),这样你的 Model 会随着 View 触发事件而改变 (view 改变的时候改变数据)

1、M 到 V 实现

做到这件事的第一步是形成类似于:

// template
var tpl = '<p>{{ text }}</p>';
// data
var data = {
text: ‘This is some text‘
};
// magic process
template(tpl, data); // '<p>This is some text</p>'

中间的 magic process 是模板引擎所做的事情,已经有非常多种模板引擎可供选择

当然你比较喜欢造轮子的话也可以自己实现一个

无论是 Angular 的 $scope,React 的 state 还是 Vue 的 data 都提供了一个较为核心的 model 对象用来保存模型的状态;它们的模板引擎稍有差别,不过大体思路相似;拿到渲染后的 string 接下来做什么不言而喻了(中间还有很多处理,例如利用 model 的 diff 来最小量更新 view )。

但是仅仅是这样并不够,我们需要知道什么时候来更新 view( 即 render ),一般来说主要的 VM 做了以下几种选择:

  • VM 实例初始化时
  • model 动态修改时

其中初始化拿到 model 对象然后 render 没什么好讲的;model 被修改的时候如何监听属性的改变是一个问题,目前有以下几种思路:

  • 借助于 Object 的 observe 方法
  • 自己在 set,以及数组的常用操作里触发 change 事件
  • 手动 setState (),然后在里面触发 change 事件

知道了触发 render 的时机以及如何 render,一个简单的 M 到 V 映射就实现了。

2、V 到 M 实现

从 V 到 M 主要由两类( 虽然本质上都是监听 DOM )构成,一类是用户自定义的 listener, 一类是 VM 自动处理的含有 value 属性元素的 listener

第一类类似于你在 Vue 里用 v-on 时绑定的那样,VM 在实例化得时候可以将所有用户自定义的 listener 一次性代理到根元素上,这些 listener 可以访问到你的 model 对象,这样你就可以在 listener 中改变 model

第二类类似于对含有 v-model 与 value 元素的自动处理,我们期望的是例如在一个输入框内

<input type="text" v-model="message" />

输入值,那么我与之对应的 model 属性 message 也会随之改变,相当于 VM 做了一个默认的 listener,它会监听这些元素的改变然后自动改变 model,具体如何实现相信你也明白了

# 第 30 题 (2019/10/18)

题目: 两个数组合并成一个数组

请把两个数组 [‘A1’, ‘A2’, ‘B1’, ‘B2’, ‘C1’, ‘C2’, ‘D1’, ‘D2’] 和 [‘A’, ‘B’, ‘C’, ‘D’],合并为 [‘A1’, ‘A2’, ‘A’, ‘B1’, ‘B2’, ‘B’, ‘C1’, ‘C2’, ‘C’, ‘D1’, ‘D2’, ‘D’]。

function MergeArray(arr1, arr2) {
  var a2 = arr2.map((item) => {
    return item + 3;
  });
  var arr = [...arr1, ...a2].sort();
  return arr.map((item) => {
    if (item.includes(3)) {
      return item.split("")[0];
    }
    return item;
  });
}

# 第 31 题 (2019/10/18)

题目: 改造下面的代码,使之输出 0 - 9,写出你能想到的所有解法。

for (var i = 0; i < 10; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}

解析:

  1. 使用闭包:
for (var i = 0; i < 10; i++) {
  ((i) => {
    setTimeout(() => {
      console.log(i);
    }, 1000);
  })(i);
}
  1. 使用 let
for (let i = 0; i < 10; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}

# 第 32 题 (2019/10/19)

题目: Virtual DOM 真的比操作原生 DOM 快吗?谈谈你的想法。

解析:

作者:尤雨溪

链接:https://www.zhihu.com/question/31809713/answer/53544875

来源:知乎

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

1. 原生 DOM 操作 vs. 通过框架封装操作。

这是一个性能 vs. 可维护性的取舍。框架的意义在于为你掩盖底层的 DOM 操作,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护。没有任何框架可以比纯手动的优化 DOM 操作更快,因为框架的 DOM 操作层需要应对任何上层 API 可能产生的操作,它的实现必须是普适的。针对任何一个 benchmark,我都可以写出比任何框架更快的手动优化,但是那有什么意义呢?在构建一个实际应用的时候,你难道为每一个地方都去做手动优化吗?出于可维护性的考虑,这显然不可能。框架给你的保证是,你在不需要手动优化的情况下,我依然可以给你提供过得去的性能。

2. 对 React 的 Virtual DOM 的误解。

React 从来没有说过 “React 比原生操作 DOM 快”。React 的基本思维模式是每次有变动就整个重新渲染整个应用。如果没有 Virtual DOM,简单来想就是直接重置 innerHTML。很多人都没有意识到,在一个大型列表所有数据都变了的情况下,重置 innerHTML 其实是一个还算合理的操作… 真正的问题是在 “全部重新渲染” 的思维模式下,即使只有一行数据变了,它也需要重置整个 innerHTML,这时候显然就有大量的浪费。

我们可以比较一下 innerHTML vs. Virtual DOM 的重绘性能消耗:

  • innerHTML: render html string O(template size) + 重新创建所有 DOM 元素 O(DOM size)
  • Virtual DOM: render Virtual DOM + diff O(template size) + 必要的 DOM 更新 O(DOM change)

Virtual DOM render + diff 显然比渲染 html 字符串要慢,但是!它依然是纯 js 层面的计算,比起后面的 DOM 操作来说,依然便宜了太多。可以看到,innerHTML 的总计算量不管是 js 计算还是 DOM 操作都是和整个界面的大小相关,但 Virtual DOM 的计算量里面,只有 js 计算和界面大小相关,DOM 操作是和数据的变动量相关的。前面说了,和 DOM 操作比起来,js 计算是极其便宜的。这才是为什么要有 Virtual DOM:它保证了 1)不管你的数据变化多少,每次重绘的性能都可以接受;2) 你依然可以用类似 innerHTML 的思路去写你的应用。

3. MVVM vs. Virtual DOM

相比起 React,其他 MVVM 系框架比如 Angular, Knockout 以及 Vue、Avalon 采用的都是数据绑定:通过 Directive/Binding 对象,观察数据变化并保留对实际 DOM 元素的引用,当有数据变化时进行对应的操作。MVVM 的变化检查是数据层面的,而 React 的检查是 DOM 结构层面的。MVVM 的性能也根据变动检测的实现原理有所不同:Angular 的脏检查使得任何变动都有固定的

O(watcher count)

的代价;Knockout/Vue/Avalon 都采用了依赖收集,在 js 和 DOM 层面都是

O(change)

  • 脏检查:scope digest O(watcher count) + 必要 DOM 更新 O(DOM change)
  • 依赖收集:重新收集依赖 O(data change) + 必要 DOM 更新 O(DOM change)

可以看到,Angular 最不效率的地方在于任何小变动都有的和 watcher 数量相关的性能代价。但是!当所有数据都变了的时候,Angular 其实并不吃亏。依赖收集在初始化和数据变化的时候都需要重新收集依赖,这个代价在小量更新的时候几乎可以忽略,但在数据量庞大的时候也会产生一定的消耗。

MVVM 渲染列表的时候,由于每一行都有自己的数据作用域,所以通常都是每一行有一个对应的 ViewModel 实例,或者是一个稍微轻量一些的利用原型继承的 “scope” 对象,但也有一定的代价。所以,MVVM 列表渲染的初始化几乎一定比 React 慢,因为创建 ViewModel /scope 实例比起 Virtual DOM 来说要昂贵很多。这里所有 MVVM 实现的一个共同问题就是在列表渲染的数据源变动时,尤其是当数据是全新的对象时,如何有效地复用已经创建的 ViewModel 实例和 DOM 元素。假如没有任何复用方面的优化,由于数据是 “全新” 的,MVVM 实际上需要销毁之前的所有实例,重新创建所有实例,最后再进行一次渲染!这就是为什么题目里链接的 angular/knockout 实现都相对比较慢。相比之下,React 的变动检查由于是 DOM 结构层面的,即使是全新的数据,只要最后渲染结果没变,那么就不需要做无用功。

Angular 和 Vue 都提供了列表重绘的优化机制,也就是 “提示” 框架如何有效地复用实例和 DOM 元素。比如数据库里的同一个对象,在两次前端 API 调用里面会成为不同的对象,但是它们依然有一样的 uid。这时候你就可以提示 track by uid 来让 Angular 知道,这两个对象其实是同一份数据。那么原来这份数据对应的实例和 DOM 元素都可以复用,只需要更新变动了的部分。或者,你也可以直接 track by $index 来进行 “原地复用”:直接根据在数组里的位置进行复用。在题目给出的例子里,如果 angular 实现加上 track by $index 的话,后续重绘是不会比 React 慢多少的。甚至在 dbmonster 测试中,Angular 和 Vue 用了 track by $index 以后都比 React 快: dbmon (注意 Angular 默认版本无优化,优化过的在下面)

顺道说一句,React 渲染列表的时候也需要提供 key 这个特殊 prop,本质上和 track-by 是一回事。

4. 性能比较也要看场合

在比较性能的时候,要分清楚初始渲染、小量数据更新、大量数据更新这些不同的场合。Virtual DOM、脏检查 MVVM、数据收集 MVVM 在不同场合各有不同的表现和不同的优化需求。Virtual DOM 为了提升小量数据更新时的性能,也需要针对性的优化,比如 shouldComponentUpdate 或是 immutable data。

  • 初始渲染:Virtual DOM > 脏检查 >= 依赖收集
  • 小量数据更新:依赖收集 >> Virtual DOM + 优化 > 脏检查(无法优化) > Virtual DOM 无优化
  • 大量数据更新:脏检查 + 优化 >= 依赖收集 + 优化 > Virtual DOM(无法 / 无需优化)>> MVVM 无优化

不要天真地以为 Virtual DOM 就是快,diff 不是免费的,batching 么 MVVM 也能做,而且最终 patch 的时候还不是要用原生 API。在我看来 Virtual DOM 真正的价值从来都不是性能,而是它 1) 为函数式的 UI 编程方式打开了大门;2) 可以渲染到 DOM 以外的 backend,比如 ReactNative。

# 第 33 题 (2019/10/20)

题目: 下面的代码打印什么内容,为什么?

var b = 10;
(function b() {
  b = 20;
  console.log(b);
})();

解析:

非严格模式:【输出函数体】

ƒ b(){
    b = 20;
    console.log(b);
}

严格模式:【报错】

//"Uncaught TypeError: Assignment to constant variable."

针对这题,在知乎上看到别人的回答说:

  1. 函数表达式与函数声明不同,函数名只在该函数内部有效,并且此绑定是常量绑定。
  2. 对于一个常量进行赋值,在 strict 模式下会报错,非 strict 模式下静默失败。
  3. IIFE 中的函数是函数表达式,而不是函数声明。

实际上,有点类似于以下代码,但不完全相同,因为使用 const 不管在什么模式下,都会 TypeError 类型的错误

const foo = function () {
foo = 10;
console.log(foo)
}
(foo)() // Uncaught TypeError: Assignment to constant variable.

我的理解是,b 函数是一个相当于用 const 定义的常量,内部无法进行重新赋值,如果在严格模式下,会报错 "Uncaught TypeError: Assignment to constant variable."
例如下面的:

var b = 10;
(function b() {
'use strict'
b = 20;
console.log(b)
})() // "Uncaught TypeError: Assignment to constant variable."

这个回答主要表达的是:函数表达式的函数名只在该函数内部有效,且绑定是常量类似 const,不能修改

# 第 34 题(2019/10/21)

题目: 简单改造下面的代码,使之分别打印 10 和 20。

var b = 10;
(function b() {
  b = 20;
  console.log(b);
})();

打印 20:

方法一:

var b = 10;
(function b(b) {
  b = 20;
  console.log(b);
})(b);

方法二:

var b = 10;
(function b() {
  var b = 20;
  console.log(b);
})();

打印 10:

方法一:(挂载在 全局 window 上)

var b = 10;
(function b(b) {
  window.b = 20;
  console.log(b);
})(b);

方法二: (挂载在 b 函数(函数也是特殊的对象)上)

var b = 10;
(function b(b) {
  b.b = 20;
  console.log(b);
})(b);

# 第 35 题 (2019/10/22)

题目:浏览器缓存读取规则?

可以分成 Service Worker、Memory Cache、Disk Cache 和 Push Cache,那请求的时候 from memory cache 和 from disk cache 的依据是什么,哪些数据什么时候存放在 Memory Cache 和 Disk Cache 中?

解析: 深入理解浏览器的缓存机制 一文读懂前端缓存

对于第一个问题前面的文章都说得很详细了我这里就不再多余述
第二个问题可以参考我写的博文 命中强制缓存时,该从哪拿缓存
小节。总的来说:

  1. 如果开启了 Service Worker 首先会从 Service Worker 中拿
  2. 如果新开一个以前打开过的页面缓存会从 Disk Cache 中拿(称为是命中强缓存)
  3. 刷新当前页面时浏览器会根据当前运行环境内存来决定是从 Memory Cache 还是从 Disk Cache 中拿(可以看到下图最后几个文件有时候是从 Memory Cache 中拿有时候是从 Disk Cache 中拿)
#

# 第 36 题 (2019/10/22)

题目: 使用迭代的方式实现 flatten 函数。

解析:

//使用迭代的方式实现flatten函数
/**
 * 使用递归的方式处理
 * wrap内保存结果ret
 * 返回一个递归函数
 *
 * @returns
 */

var arr = [1, 2, 3, [4, 5], [6, [7, [8]]]];
console.log(wrap()(arr));

function wrap() {
  var ret = [];
  return function flatten(arr) {
    for (let item of arr) {
      if (item.constructor === Array) {
        ret.concat(flatten(item));
      } else {
        ret.push(item);
      }
    }
    return ret;
  };
}

# 第 37 题 (2019/10/22)

题目: 为什么 Vuex 的 mutation 和 Redux 的 reducer 中不能做异步操作?

解析: 待续…

# 第 38 题 (2019/10/23)

题目: 下面代码中 a 在什么情况下会打印 1?

var a = ?;
if(a == 1 && a == 2 && a == 3){
 	console.log(1);
}

公司: 京东

考点: 隐式类型转换

解析: 从 (a1&&a2&&a==3) 成立中看 javascript 的隐式类型转换

关于 === 于 ==

但是我比较喜欢的一本书 You don't know JS , 中作者也写道过一个我比较赞同的观点

很多开发者认为 === 的行为更加容易预测,从而主张使用 === 而远离 。我认为这种观点是非常短视的,如果你花点时间去搞清楚它的工作原理, 将是你开发的强大工具

  1. 运算子是对象时候的 valueOf toString 方法
const a = {
  i: 1,
  toString: function () {
    return a.i++;
  },
};

if (a == 1 && a == 2 && a == 3) {
  console.log("1"); //1
  console.log(a.i); //4
}

如果原始类型的值和对象比较,对象会转为原始类型的值,再进行比较。 (我想到的也是这种方法) ,对象转换成原始类型的值,算法是先调用 valueOf 方法;如果返回的还是对象,再接着调用 toString 方法。我们每次比较时候都会执行方法返回 ai 属性同时也改变 i 的值,所以上面 if 执行完以后 ai 属性已经变为了 4,这里也表现出了 == 比较是有可能会对变量带来副作用的

利用数组的特性

var a = [1, 2, 3];
a.join = a.shift;

if (a == 1 && a == 2 && a == 3) {
  console.log("1");
}

这个答案还是比较巧妙的,我们知道 array 也属于对象,应该和对象的规则一样。关于 array 的原型链上的 toString 方法

对于数组对象,toString 方法返回一个字符串,该字符串由数组中的每个元素的 toString () 返回值经调用 join () 方法连接(由逗号隔开)组成。

可以看到数组 toString 会调用本身的 join 方法,这里把自己的 join 方法该写为 shift , 每次返回第一个元素,而且原数组删除第一个值,正好可以使判断成立。这里 == 比较也带来的副作用

利用 with 关键字

var i = 0;

with ({
  get a() {
    return ++i;
  },
}) {
  if (a == 1 && a == 2 && a == 3) console.log("1");
}

with 也是被严重建议不使用的对象,这里也是利用它的特性在代码块里面利用对象的 get 方法动态返回 i .

和 with 类似修改 window 的 get 方法

var val = 0;
Object.defineProperty(window, "a", {
  get: function () {
    return ++val;
  },
});
if (a == 1 && a == 2 && a == 3) {
  console.log("yay");
}

我们知道我们用的全局变量也相当于 window 对象上的一个属性,这里用 defineProperty 定义了 aget 也使得其动态返回值。和 with 有一些类似。

es6 的 Symbol 特性

let a = { [Symbol.toPrimitive]: ((i) => () => ++i)(0) };

console.log(a == 1 && a == 2 && a == 3);

ES6 引入了一种新的原始数据类型 Symbol ,表示独一无二的值。我们之前在定义类的内部私有属性时候习惯用 __xxx , 这种命名方式避免别人定义相同的属性名覆盖原来的属性,有了 Symbol 之后我们完全可以用 Symbol 值来代替这种方法,而且完全不用担心被覆盖。

除了定义自己使用的 Symbol 值以外, ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。 Symbol.toPrimitive 就是其中一个,它指向一个方法,表示该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。这里就是改变这个属性,把它的值改为一个 闭包 返回的函数。

# 第 39 题 (2019/10/24)

题目: 介绍下 BFC 及其应用

解析:

BFC 就是块级格式上下文,是页面盒模型布局中的一种 CSS 渲染模式,相当于一个独立的容器,里面的元素和外部的元素相互不影响。创建 BFC 的方式有:

  1. html 根元素
  2. float 浮动
  3. 绝对定位
  4. overflow 不为 visiable
  5. display 为表格布局或者弹性布局

BFC 主要的作用是:

  1. 清除浮动
  2. 防止同一 BFC 容器中的相邻元素间的外边距重叠问题

# 第 40 题 (2019/10/25)

题目: 在 Vue 中,子组件为何不可以修改父组件传递的 Prop?

如果修改了,Vue 是如何监控到属性的修改并给出警告的。

解析:

  1. 子组件为何不可以修改父组件传递的 Prop
    单向数据流,易于监测数据的流动,出现了错误可以更加迅速的定位到错误发生的位置。
  2. 如果修改了,Vue 是如何监控到属性的修改并给出警告的。
if (process.env.NODE_ENV !== "production") {
  var hyphenatedKey = hyphenate(key);
  if (
    isReservedAttribute(hyphenatedKey) ||
    config.isReservedAttr(hyphenatedKey)
  ) {
    warn(
      '"' +
        hyphenatedKey +
        '" is a reserved attribute and cannot be used as component prop.',
      vm
    );
  }
  defineReactive$$1(props, key, value, function () {
    if (!isRoot && !isUpdatingChildComponent) {
      warn(
        "Avoid mutating a prop directly since the value will be " +
          "overwritten whenever the parent component re-renders. " +
          "Instead, use a data or computed property based on the prop's " +
          'value. Prop being mutated: "' +
          key +
          '"',
        vm
      );
    }
  });
}

在 initProps 的时候,在 defineReactive 时通过判断是否在开发环境,如果是开发环境,会在触发 set 的时候判断是否此 key 是否处于 updatingChildren 中被修改,如果不是,说明此修改来自子组件,触发 warning 提示。

需要特别注意的是,当你从子组件修改的 prop 属于基础类型时会触发提示。 这种情况下,你是无法修改父组件的数据源的, 因为基础类型赋值时是值拷贝。你直接将另一个非基础类型(Object, array)赋值到此 key 时也会触发提示 (但实际上不会影响父组件的数据源), 当你修改 object 的属性时不会触发提示,并且会修改父组件数据源的数据。

# 第 41 题 (2019/10/25)

题目: 下面代码输出什么

var a = 10;
(function () {
  console.log(a); // undefined
  a = 5;
  console.log(window.a); // 10
  var a = 20;
  console.log(a); //20
})();

解析: 分别为 undefined   10   20,原因是作用域问题,在内部声名 var a = 20; 相当于先声明 var a; 然后再执行赋值操作,这是在IIFE内形成的独立作用域,如果把 var a=20 注释掉,那么 a 只有在外部有声明,显示的就是外部的A变量的值了。结果A会是 10   5   5

# 第 42 题 (2019/10/25)

题目: 实现一个 sleep 函数

比如 sleep (1000) 意味着等待 1000 毫秒,可从 Promise、Generator、Async/Await 等角度实现

//Promise1
const sleep = (time) => {
  return new Promise((resolve) => setTimeout(resolve, time));
};
sleep(1000).then(() => {
  console.log(1);
});

//Generator
function* sleepGenerator(time) {
  yield new Promise(function (resolve, reject) {
    setTimeout(resolve, time);
  });
}
sleepGenerator(1000)
  .next()
  .value.then(() => {
    console.log(1);
  });

//async
function sleep(time) {
  return new Promise((resolve) => setTimeout(resolve, time));
}
async function output() {
  let out = await sleep(1000);
  console.log(1);
  return out;
}
output();

//ES5
function sleep(callback, time) {
  if (typeof callback === "function") setTimeout(callback, time);
}

function output() {
  console.log(1);
}
sleep(output, 1000);

参考:

# 第 43 题 (2019/10/26)

题目: 使用 sort () 对数组 [3, 15, 8, 29, 102, 22] 进行排序,输出结果

解析:

原题目:

使用 sort () 对数组 [3, 15, 8, 29, 102, 22] 进行排序,输出结果

我的答案:

[102, 15, 22, 29, 3, 8];

解析:

根据 MDN 上对 Array.sort() 的解释,默认的排序方法会将数组元素转换为字符串,然后比较字符串中字符的 UTF-16 编码顺序来进行排序。所以 '102' 会排在 '15' 前面。以下是 MDN 中的解释原文:

The sort() method sorts the elements of an array in place and returns the array. The default sort order is built upon converting the elements into strings, then comparing their sequences of UTF-16 code units values.

# 第 44 题 (2019/10/26)

题目: 介绍 HTTPS 握手过程

解析: ~~

# 第 45 题 (2019/10/26)

题目: HTTPS 握手过程中,客户端如何验证证书的合法性

解析: ~~

# 第 46 题 (2019/10/26)

题目: 输出以下代码执行的结果并解释为什么

var obj = {
  2: 3,
  3: 4,
  length: 2,
  splice: Array.prototype.splice,
  push: Array.prototype.push,
};
obj.push(1);
obj.push(2);
console.log(obj);
/*
结果:
    Object(4) [empty × 2, 1, 2, splice: ƒ, push: ƒ]
    2: 1
    3: 2
    length: 4
    push: ƒ push()
    splice: ƒ splice()
    __proto__: Object
*/

我的理解是这样的
1: call push 这个方法如果对象有 length 属性,length 属性会加 1 并且返回,这个是在某本书的上看到的,一直记得。
MDN

push 方法将值追加到数组中。

push 方法有意具有通用性。该方法和 call () 或 apply () 一起使用时,可应用在类似数组的对象上。push 方法根据 length 属性来决定从哪里开始插入给定的值。如果 length 不能被转成一个数值,则插入的元素索引为 0,包括 length 不存在时。当 length 不存在时,将会创建它。

唯一的原生类数组(array-like)对象是 Strings,尽管如此,它们并不适用该方法,因为字符串是不可改变的。

  1. 调用 push 方法的时候会在调用对象的 key=length 的地方做一个赋值,不管前面 key 有没有值,也就是说在调用 push 的时候 对象实际被理解为了 [0:undefined,1:undefined,2:3,3:4],
    这样也就有了结果里面的
    key=2 value =1
    key
    =3 value =2 3. 额外的
    这个对象如果有 push 和 splice 会输出会转换为数组,下图为去掉 splice

包含 splice 方法

# 第 47 题 (2019/10/27)

题目: 双向绑定和 vuex 是否冲突

解析:

在严格模式下直接使用确实会有问题。
解决方案:

官网说的比较详细
https://vuex.vuejs.org/zh/guide/forms.html

<input v-model="message" />
computed: {
    message: {
        set (value) {
            this.$store.dispatch('updateMessage', value);
        },
        get () {
            return this.$store.state.obj.message
        }
    }
}
mutations: {
    UPDATE_MESSAGE (state, v) {
        state.obj.message = v;
    }
}
actions: {
    update_message ({ commit }, v) {
        commit('UPDATE_MESSAGE', v);
    }
}

# 第 48 题 (2019/10/27)

题目: call 和 apply 的区别是什么,哪个性能更好一些

解析:

**call()** 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

apply() 方法调用一个具有给定 this 值的函数,以及作为一个数组(或类似数组对象)提供的参数。

  1. Function.prototype.apply 和 Function.prototype.call 的作用是一样的,区别在于传入参数的不同;
  2. 第一个参数都是,指定函数体内 this 的指向;
  3. 第二个参数开始不同,apply 是传入带下标的集合,数组或者类数组,apply 把它传给函数作为参数,call 从第二个开始传入的参数是不固定的,都会传给函数作为参数
  4. call 比 apply 的性能要好,平常可以多用 call, call 传入参数的格式正是内部所需要的格式,参考 call 和 apply 的性能对比

# 第 49 题 (2019/10/27)

题目: 为什么通常在发送数据埋点请求的时候使用的是 1x1 像素的透明 gif 图片?

解析: 数据埋点是什么?设置埋点的意义是什么?

1. 埋点是什么?

所谓 “埋点”,是 数据采集领域(尤其是用户行为数据采集领域)的术语,指的是针对特定用户行为或事件进行捕获、处理和发送的相关技术及其实施过程。比如用户某个 icon 点击次数、观看某个视频的时长等等。

埋点的技术实质,是先监听软件应用运行过程中的事件,当需要关注的事件发生时进行判断和捕获。

特别注意需要明确事件发生时间点、判别条件,这里如果遇到不清楚的,需要和开发沟通清楚,避免采集数据与理想存在差异。例如:期望采集某个 app 的某个广告的有效曝光数,有效曝光的判别条件是停留时长超过 1 秒且有效加载出广告内容。

解答:

作用:工作中,用于前端监控,比如曝光等等,谷歌和百度的都是用的 1x1 像素的透明 gif 图片;
why?

  1. 没有跨域问题,一般这种上报数据,代码要写通用的;(排除 ajax)
  2. 不会阻塞页面加载,影响用户的体验,只要 new Image 对象就好了;(排除 JS/CSS 文件资源方式上报)
  3. 在所有图片中,体积最小;(比较 PNG/JPG)

# 第 50 题 (2019/10/28)

题目: 实现 (5).add (3).minus (2) 功能。

例: 5 + 3 - 2,结果为 6

公司:百度

解析:

Number.prototype.add = function (num) {
  return this.valueOf() + num;
};

Number.prototype.minus = function (num) {
  return this.valueOf() - num;
};

console.log((5).add(3).minus(2));

# 第 51 题 (2019/10/28)

题目: Vue 的响应式原理中 Object.defineProperty 有什么缺陷?

为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?

解析: 不懂~~~

# 第 52 题 (2019/10/28)

题目: 怎么让一个 div 水平垂直居中

解析:

<div class="parent">
  <div class="child"></div>
</div>

1、利用 flex

div.parent {
  display: flex;
  justify-content: center;
  align-items: center;
}

2、绝对定位

div.parent {
  position: relative;
}
div.child {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
/* 或者 */
div.child {
  width: 50px;
  height: 10px;
  position: absolute;
  top: 50%;
  left: 50%;
  margin-left: -25px;
  margin-top: -5px;
}
/* 或 */
div.child {
  width: 50px;
  height: 10px;
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
  margin: auto;
}

3、网格布局

div.parent {
  display: grid;
}
div.child {
  justify-self: center;
  align-self: center;
}

4、

div.parent {
  font-size: 0;
  text-align: center;
  &::before {
    content: "";
    display: inline-block;
    width: 0;
    height: 100%;
    vertical-align: middle;
  }
}
div.child {
  display: inline-block;
  vertical-align: middle;
}

5、补充

div.parent {
  display: flex;
}
div.child {
  margin: auto;
}
div.parent {
display: table;
}
div.child {
display: table-cell
vertical-align: middle;
text-align: center;
}

# 第 53 题 (2019/10/29)

题目: 输出以下代码的执行结果并解释为什么

var a = { n: 1 };
var b = a;
a.x = a = { n: 2 };

console.log(a.x); //undefined
console.log(b.x); //{ n: 2 }
console.log(a); //{ n: 2 }
console.log(b); //{ n: 1, x: { n: 2 } }

解析:

首先,a和b同时引用了{n:2}对象,接着执行到a.x = a = {n:2}语句,尽管赋值是从右到左的没错,但是.的优先级比=要高,所以这里首先执行a.x,相当于为a(或者b)所指向的{n:1}对象新增了一个属性x,即此时对象将变为{n:1;x:undefined}。之后按正常情况,从右到左进行赋值,此时执行a ={n:2}的时候,a的引用改变,指向了新对象{n:2},而b依然指向的是旧对象。之后执行a.x = {n:2}的时候,并不会重新解析一遍a,而是沿用最初解析a.x时候的a,也即旧对象,故此时旧对象的x的值为{n:2},旧对象为 {n:1;x:{n:2}},它被b引用着。
后面输出a.x的时候,又要解析a了,此时的a是指向新对象的a,而这个新对象是没有x属性的,故访问时输出undefined;而访问b.x的时候,将输出旧对象的x的值,即{n:2}。

# 第 54 题 (2019/10/29)

题目: 冒泡排序如何实现,时间复杂度是多少, 还可以如何改进?

解析:

// 冒泡排序
function BubbleSort(nums, n) {
  if (nums == null || nums.length < 2) return nums;
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n - i - 1; j++) {
      if (nums[j + 1] < nums[j]) {
        [nums[j], nums[j + 1]] = [nums[j + 1], nums[j]];
      }
    }
  }
  return nums;
}

性质:

1、时间复杂度:O (n^2)

2、空间复杂度:O (1)

3、稳定排序

4、原地排序

// 改进冒泡排序
function bubbleSort1(arr) {
  let i = arr.length - 1;

  while (i > 0) {
    let pos = 0;
    for (let j = 0; j < i; j++) {
      if (arr[j] > arr[j + 1]) {
        pos = j;
        const temp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = temp;
      }
    }
    i = pos;
  }
  console.log(arr);
}

# 第 55 题 (2019/10/30)

题目: 某公司 1 到 12 月份的销售额存在一个对象里面

如下:{1:222, 2:123, 5:888},请把数据处理为如下结构:[222, 123, null, null, 888, null, null, null, null, null, null, null]。

**Array.from()** 方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。

var obj = {1: 222, 2: 123, 5: 888}
function f(obj) {
    obj.length = 13
    return Array.from(obj).slice(1).map(item = > {
        return  item === undefined ? null : item
    })
}
console.log(f(obj))
//[ 222, 123, null, null, 888, null, null, null, null, null, null, null ]

# 第 56 题 (2019/10/30)

题目: 要求设计 LazyMan 类,实现以下功能。

考点:数据结构

LazyMan("Tony");
// Hi I am Tony

LazyMan("Tony").sleep(10).eat("lunch");
// Hi I am Tony
// 等待了10秒...
// I am eating lunch

LazyMan("Tony").eat("lunch").sleep(10).eat("dinner");
// Hi I am Tony
// I am eating lunch
// 等待了10秒...
// I am eating diner

LazyMan("Tony")
  .eat("lunch")
  .eat("dinner")
  .sleepFirst(5)
  .sleep(10)
  .eat("junk food");
// Hi I am Tony
// 等待了5秒...
// I am eating lunch
// I am eating dinner
// 等待了10秒...
// I am eating junk food

解析: ~~~

# 第 57 题(2019/10/31)

题目: 分析比较 opacity: 0、visibility: hidden、display: none 优劣和适用场景。

解析:

总结一下:

结构:
display:none: 会让元素完全从渲染树中消失,渲染的时候不占据任何空间,不能点击,
visibility: hidden: 不会让元素从渲染树消失,渲染元素继续占据空间,只是内容不可见,不能点击
opacity: 0: 不会让元素从渲染树消失,渲染元素继续占据空间,只是内容不可见,可以点击

继承:
display: none 和 opacity: 0:是非继承属性,子孙节点消失由于元素从渲染树消失造成,通过修改子孙节点属性无法显示。
visibility: hidden:是继承属性,子孙节点消失由于继承了 hidden,通过设置 visibility: visible; 可以让子孙节点显式。

性能:
displaynone : 修改元素会造成文档回流,读屏器不会读取 display: none 元素内容,性能消耗较大
visibility:hidden: 修改元素只会造成本元素的重绘,性能消耗较少读屏器读取 visibility: hidden 元素内容
opacity: 0 : 修改元素会造成重绘,性能消耗较少

联系 :它们都能让元素不可见

# 第 58 题(2019/10/31)

题目: 第 58 题:箭头函数与普通函数(function)的区别是什么?构造函数(function)可以使用 new 生成实例,那么箭头函数可以吗?为什么?

解析:

引入箭头函数有两个方面的作用:更简短的函数并且不绑定 this

箭头函数是普通函数的简写,可以更优雅的定义一个函数,和普通函数相比,有以下几点差异:

  1. 箭头函数没有 this,它会从自己的作用域链的上一层继承 this(因此无法使用 apply /call/bind 进行绑定 this 值);
  2. 不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
  3. 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。
  4. 无法使用 new 实例化对象,因为普通构造函数通过 new 实例化对象时 this 指向实例对象,而箭头函数没有 this 值,同时 箭头函数也没有 prototype。

new 过程大致是这样的:

function newFunc(father, ...rest) {
  var result = {};
  result.__proto__ = father.prototype;
  var result2 = father.apply(result, rest);
  if (
    (typeof result2 === "object" || typeof result2 === "function") &&
    result2 !== null
  ) {
    return result2;
  }
  return result;
}

# 第 59 题 (2019/10/31)

题目: 给定两个数组,写一个方法来计算它们的交集。

例如:给定 nums1 = [1, 2, 2, 1],nums2 = [2, 2],返回 [2, 2]。

var nums1 = [1, 2, 2, 1],
  nums2 = [2, 2, 3, 4];
// 1.
// 有个问题, [NaN].indexOf(NaN) === -1
var newArr1 = nums1.filter(function (item) {
  return nums2.indexOf(item) > -1;
});

// 2.
var newArr2 = nums1.filter((item) => {
  return nums2.includes(item);
});

# 第 60 题 (2019/10/31)

题目: 已知如下代码,如何修改才能让图片宽度为 300px ?注意下面代码不可修改。

<img src=“1.jpg” style="width:480px!important;”>

解决方案:

  1. max-width: 300px
  2. transform: scale(0.625,0.625)
  3. zoom: 0.625 ; MDN:zoom
  4. 解法:
box-sizing: border-box;
padding: 0 90px;
  1. js: document.getElementsByTagName('img')[0].style.width='300px'

# 第 61 题 (2019/11/01)

题目: 介绍下如何实现 token 加密?

解析:

这个题目是问:生成 token 的方法,比如 JWT,还是说利用加密算法,比如对称加密或者非对称加密 加密生成后的 token ?

这边也是这么做的,后端根据 token 来查权限和是否登录以及失效等

token 加密方式:

  • 服务器通过私钥对一部分信息进行加密生成签名,并将签名和数据拼接在一起作为 token 的一部分。例如 JWT。
  • 使用客户端的 UA 或其他数据作为干扰码对 token 进行加密。

相关参考文章:

jwt 举例

  1. 需要一个 secret(随机数)
  2. 后端利用 secret 和加密算法 (如:HMAC-SHA256) 对 payload (如账号密码) 生成一个字符串 (token),返回前端
  3. 前端每次 request 在 header 中带上 token
  4. 后端用同样的算法解密

这边也是这么做的,后端根据 token 来查权限和是否登录以及失效等

# 第 62 题 (2019/11/01)

题目: redux 为什么要把 reducer 设计成纯函数

解析: 学习 react ~~

# 第 63 题(2019/11/02)

题目: 如何设计实现无缝轮播如何设计实现无缝轮播

解析:

无限轮播基本插件都可以做到,不过要使用原生代码实现无缝滚动的话我可以提点思路,
因为轮播图基本都在ul盒子里面的li元素,
首先获取第一个li元素和最后一个li元素,
克隆第一个li元素,和最后一个li元素,
分别插入到lastli的后面和firstli的前面,
然后监听滚动事件,如果滑动距离超过x或-x,让其实现跳转下一张图或者跳转上一张,(此处最好设置滑动距离),
然后在滑动最后一张实现最后一张和克隆第一张的无缝转换,当到克隆的第一张的时候停下的时候,,让其切入真的第一张,则实现无线滑动,向前滑动同理

# 第 64 题(2019/11/02)

** 题目:** 模拟实现一个 Promise.finally

知识点 :异步

解析:

# 第 65 题 (2019/11/02)

题目: a.b.c.da['b']['c']['d'] ,哪个性能更高?

解析:

应该是 a.b.c.da['b']['c']['d'] 性能高点,后者还要考虑 [ ] 中是变量的情况,再者,从两种形式的结构来看,显然编译器解析前者要比后者容易些,自然也就快一点。
下图是两者的 AST (抽象语法树) 对比:

# 第 66 题 (2019/11/02)

题目: ES6 代码转成 ES5 代码的实现思路是什么

解析:

回到正题上来,说到 ES6 代码转成 ES5 代码,我们肯定会想到 Babel。所以,我们可以参考 Babel 的实现方式。

那么 Babel 是如何把 ES6 转成 ES5 呢,其大致分为三步:

  • 将代码字符串解析成抽象语法树,即所谓的 AST
  • AST 进行处理,在这个阶段可以对 ES6 代码进行相应转换,即转成 ES5 代码
  • 根据处理后的 AST 再生成代码字符串

基于此,其实我们自己就可以实现一个简单的 “编译器”,用于把 ES6 代码转成 ES5。

比如,可以使用 @babel/parserparse 方法,将代码字符串解析成 AST;使用 @babel/coretransformFromAstSync 方法,对 AST 进行处理,将其转成 ES5 并生成相应的代码字符串;过程中,可能还需要使用 @babel/traverse 来获取依赖文件等。对此感兴趣的可以看看这个

# 第 67 题 (2019/11/03)

题目: 数组编程题

随机生成一个长度为 10 的整数类型的数组,例如 [2, 10, 3, 4, 5, 11, 10, 11, 20] ,将其排列成一个新数组,要求新数组形式如下,例如 [[2, 3, 4, 5], [10, 11], [20]]

/*
	我理解是:去重排序数组后,分类连续数列。
*/
let initArr = Array.from({ length: 10 }, (v) => {
  return getRandomIntInclusive(0, 20);
});

// 得到一个两数之间的随机整数,包括两个数在内
function getRandomIntInclusive(min, max) {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min + 1)) + min; //含最大值,含最小值
}

function GetArr(arr) {
  var newarr = Array.from(new Set(arr)).sort((a, b) => a - b);
  var pre = 0,
    cur = 1;
  var count = 1;
  var xarr = [newarr[0]];
  var Finllyarr = [];
  while (cur <= newarr.length) {
    if (newarr[cur] - newarr[pre] === count) {
      xarr.push(newarr[cur]);
    } else {
      pre = cur;
      count = 0;
      Finllyarr.push(xarr);
      xarr = [newarr[pre]];
    }
    cur++;
    count++;
  }
  return Finllyarr;
}
console.log(GetArr(initArr));

# 第 68 题 (2019/11/04)

题目: 如何解决移动端 Retina 屏 1px 像素问题

解析: 7 种方法解决移动端 Retina 屏幕 1px 边框问题

  1. 0.5px 边框
  2. 使用 border-image 实现
  3. 使用 background-image 实现
  4. 多背景渐变实现
  5. 使用 box-shadow 模拟边框
  6. viewport + rem 实现
  7. 伪类 + transform 实现

# 第 69 题 (2019/11/07)

题目: 如何把一个字符串的大小写取反(大写变小写小写变大写),例如 ’AbC’ 变成 ‘aBc’ 。

解析

  1. 利用 ASCII 码 (A: 65 ,Z:90,a:97,z:122)
function Getstr(str) {
  return str
    .split("")
    .map((item) => {
      if (item.charCodeAt() <= 90 && item.charCodeAt() >= 65)
        return item.toLowerCase();
      else return item.toUpperCase();
    })
    .join("");
}
  1. 利用 小技巧
function Getstr(str) {
  return str
    .split("")
    .map((item) => {
      return item === item.toUpperCase()
        ? item.toLowerCase()
        : item.toUpperCase();
    })
    .join("");
}

# 第 70 题 (2019/11/08)

题目: 介绍下 webpack 热更新原理,是如何做到在不刷新浏览器的前提下更新页面的

解析:

1.当修改了一个或多个文件;
2.文件系统接收更改并通知webpack;
3.webpack重新编译构建一个或多个模块,并通知HMR服务器进行更新;
4.HMR Server 使用webSocket通知HMR runtime 需要更新,HMR运行时通过HTTP请求更新jsonp;
5.HMR运行时替换更新中的模块,如果确定这些模块无法更新,则触发整个页面刷新。

# 第 71 题 (2019/11/14)

题目: 实现一个字符串匹配算法,从长度为 n 的字符串 S 中,查找是否存在字符串 T,T 的长度是 m,若存在返回所在位置。

解析:

// 因为 T 的 length 是一定的,所以在循环S的的时候 ,循环当前项 i 后面至少还有 T.length 个元素
const find = (S, T) => {
  if (S.length < T.length) return -1;
  for (let i = 0; i < S.length - T.length; i++) {
    if (S.substr(i, T.length) === T) return i;
  }
  return -1;
};
// 方法一:
const find = (S, T) => S.search(T);

// 方法二:
const find = (S, T) => {
  const matched = S.match(T);
  return matched ? matched.index : -1;
};

# 第 72 题 (2019/11/15)

** 题目:** 为什么普通 for 循环的性能远远高于 forEach 的性能,请解释其中的原因。

解析:

  • for 循环没有任何额外的函数调用栈和上下文;
  • forEach 函数签名实际上是
array.forEach(function(currentValue, index, arr), thisValue)

它不是普通的 for 循环的语法糖,还有诸多参数和上下文需要在执行的时候考虑进来,这里可能拖慢性能;

# 第 73 题 (2019/11/15)

题目: 介绍下 BFC、IFC、GFC 和 FFC

解析:

BFC(Block formatting contexts):块级格式上下文
页面上的一个隔离的渲染区域,那么他是如何产生的呢?可以触发 BFC 的元素有 float、position、overflow、display:table-cell/inline-block/table-caption ;BFC 有什么作用呢?比如说实现多栏布局’

IFC(Inline formatting contexts):内联格式上下文
IFC 的 line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的 padding/margin 影响) IFC 中的 line box 一般左右都贴紧整个 IFC,但是会因为 float 元素而扰乱。float 元素会位于 IFC 与与 line box 之间,使得 line box 宽度缩短。 同个 ifc 下的多个 line box 高度会不同
IFC 中时不可能有块级元素的,当插入块级元素时(如 p 中插入 div)会产生两个匿名块与 div 分隔开,即产生两个 IFC,每个 IFC 对外表现为块级元素,与 div 垂直排列。
那么 IFC 一般有什么用呢?
水平居中:当一个块要在环境中水平居中时,设置其为 inline-block 则会在外层产生 IFC,通过 text-align 则可以使其水平居中。
垂直居中:创建一个 IFC,用其中一个元素撑开父元素的高度,然后设置其 vertical-align:middle,其他行内元素则可以在此父元素下垂直居中。

GFC(GrideLayout formatting contexts):网格布局格式化上下文
当为一个元素设置 display 值为 grid 的时候,此元素将会获得一个独立的渲染区域,我们可以通过在网格容器(grid container)上定义网格定义行(grid definition rows)和网格定义列(grid definition columns)属性各在网格项目(grid item)上定义网格行(grid row)和网格列(grid columns)为每一个网格项目(grid item)定义位置和空间。那么 GFC 有什么用呢,和 table 又有什么区别呢?首先同样是一个二维的表格,但 GridLayout 会有更加丰富的属性来控制行列,控制对齐以及更为精细的渲染语义和控制。

FFC(Flex formatting contexts): 自适应格式上下文
display 值为 flex 或者 inline-flex 的元素将会生成自适应容器(flex container),可惜这个牛逼的属性只有谷歌和火狐支持,不过在移动端也足够了,至少 safari 和 chrome 还是 OK 的,毕竟这俩在移动端才是王道。Flex Box 由伸缩容器和伸缩项目组成。通过设置元素的 display 属性为 flex 或 inline-flex 可以得到一个伸缩容器。设置为 flex 的容器被渲染为一个块级元素,而设置为 inline-flex 的容器则渲染为一个行内元素。伸缩容器中的每一个子元素都是一个伸缩项目。伸缩项目可以是任意数量的。伸缩容器外和伸缩项目内的一切元素都不受影响。简单地说,Flexbox 定义了伸缩容器内伸缩项目该如何布局。

在这

# 第 74 题 (2019/11/16)

题目: 使用 JavaScript Proxy 实现简单的数据绑定

解析: ~~

# 第 75 题 (2019/11/17)

题目: 数组里面有 10 万个数据,取第一个元素和第 10 万个元素的时间相差多少

解析:

考点: JavaScript 数组底层原理

数组可以直接根据索引取的对应的元素,所以不管取哪个位置的元素的时间复杂度都是 O (1)

得出结论:消耗时间几乎一致,差异可以忽略不计

Chrome 浏览器 JS 引擎 V8 中,数组有两种存储模式,一种是类似 C 语言中的线性结构存储(索引值连续,且都是正整数的情况下),一种是采用 Hash 结构存储(索引值为负数,数组稀疏,间隔比较大);

JavaScript 没有真正意义上的数组,所有的数组其实是对象,其 “索引” 看起来是数字,其实会被转换成字符串,作为属性名(对象的 key)来使用。所以无论是取第 1 个还是取第 10 万个元素,都是用 key 精确查找哈希表的过程,其消耗时间大致相同。

推荐一下这篇文章:深究 JavaScript 数组

# 第 76 题 (2019/11/17)

输出以下代码运行结果

考点 :这题考察的是对象的键名的转换。

  • 对象的键名只能是字符串和 Symbol 类型。
  • 其他类型的键名会被转换成字符串类型。
  • 对象转字符串默认会调用 toString 方法。
// example 1
var a={}, b='123', c=123;
a[b]='b';
a[c]='c';
console.log(a[b]);  //c

---------------------
// example 2
var a={}, b=Symbol('123'), c=Symbol('123');
a[b]='b';
a[c]='c';
console.log(a[b]); //b

---------------------
// example 3
var a={}, b={key:'123'}, c={key:'456'};
a[b]='b';
a[c]='c';
console.log(a[b]); //c

解析:

// example 1
var a={}, b='123', c=123;
a[b]='b';

// c 的键名会被转换成字符串'123',这里会把 b 覆盖掉。
a[c]='c';

// 输出 c
console.log(a[b]);

---------------------

// example 2
var a={}, b=Symbol('123'), c=Symbol('123');

// b 是 Symbol 类型,不需要转换。
a[b]='b';

// c 是 Symbol 类型,不需要转换。任何一个 Symbol 类型的值都是不相等的,所以不会覆盖掉 b。
a[c]='c';

// 输出 b
console.log(a[b]);

---------------------

// example 3
var a={}, b={key:'123'}, c={key:'456'};

// b 不是字符串也不是 Symbol 类型,需要转换成字符串。
// 对象类型会调用 toString 方法转换成字符串 [object Object]。
a[b]='b';

// c 不是字符串也不是 Symbol 类型,需要转换成字符串。
// 对象类型会调用 toString 方法转换成字符串 [object Object]。这里会把 b 覆盖掉。
a[c]='c';

// 输出 c
console.log(a[b]);

# 第 77 题 (2019/11/18)

题目:

给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。

示例 1:

输入: [1, 2, 3, 4, 5, 6, 7] 和 k = 3
输出: [5, 6, 7, 1, 2, 3, 4]
解释:
向右旋转 1: [7, 1, 2, 3, 4, 5, 6]
向右旋转 2: [6, 7, 1, 2, 3, 4, 5]
向右旋转 3: [5, 6, 7, 1, 2, 3, 4]

示例 2:

输入: [-1, -100, 3, 99] 和 k = 2
输出: [3, 99, -1, -100]
解释:
向右旋转 1: [99, -1, -100, 3]
向右旋转 2: [3, 99, -1, -100

解析:

function rotateArr(arr, k) {
  for (var i = 0; i < k; i++) {
    arr.unshift(arr.pop());
  }
  return arr;
}

# 第 78 题 (2019/11/18)

题目 :Vue 的父组件和子组件生命周期钩子执行顺序是什么

解析:

总结:从外到内,再从内到外

  1. 加载渲染过程
    父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
  2. 子组件更新过程
    父beforeUpdate->子beforeUpdate->子updated->父updated
  3. 父组件更新过程
    父beforeUpdate->父updated
  4. 销毁过程
    父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

# 第 79 题 (2019/11/18)

题目: input 搜索如何防抖,如何处理中文输入

解析: input 事件中文触发多次问题研究

简易防抖:

function debounce(fn, delay) {
  let timeout = null;
  return function () {
    clearTimeout(timeout);
    timeout = setTimeout(function () {
      fn.apply(this, arguments);
    }, delay);
  };
}

# 第 80 题 (2019/11/19)

题目: 介绍下 Promise.all 使用、原理实现及错误处理

# 第 81 题 (2019/11/19)

题目: 打印出 1 - 10000 之间的所有对称数

例如:121、1331 等

function f(num) {
  return num.toString() === num.toString().split("").reverse().join("");
}

for (var i = 1; i <= 10000; i++) {
  if (f(i)) {
    console.log(i);
  }
}

# 第 82 题 (2019/11/19)

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

示例:

输入: [0, 1, 0, 3, 12];
输出: [1, 3, 12, 0, 0];

说明:

  1. 必须在原数组上操作,不能拷贝额外的数组。
  2. 尽量减少操作次数。

解析:

function Movezero(arr) {
  var index = 0;
  for (var i = 0; i < arr.length; i++) {
    if (arr[i] != 0) {
      arr[index++] = arr[i];
    }
  }
  while (index < arr.length) {
    arr[index++] = 0;
  }
  return arr;
}

# 第 83 题 (2019/11/20)

题目: var、let 和 const 区别的实现原理是什么

解析:

区别:

  1. var 声明的变量会挂载在 window 上,而 let 和 const 声明的变量不会

  2. var 声明变量存在变量提升,let 和 const 不存在变量提升

  3. let 和 const 声明形成块作用域,而 var 不存在此作用域

  4. 同一作用域下 let 和 const 不能声明同名变量,而 var 可以

  5. let、const 存在暂存死区

  6. const

    1. 一旦声明必须赋值,不能使用 null 占位。
    2. 声明后不能再修改
    3. 如果声明的是复合类型数据,可以修改其属性 *

var、let、const 实现原理

记得 JS 权威指南中有一句很精辟的描述: ”JavaScript 中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里.”

以下属于推测,在网上没查到确凿的原理机制(若有误望指正):

原理大概是:在 js 解析的时候,优先解析 const,因为它不能修改的是栈内存在的值和地址。然后解析 let 因为没有块作用域可能底层有处理,最后解析 var

# 第 84 题 (2019/11/21)

题目: 请实现一个 add 函数,满足以下功能。

知识点 :函数柯里化 题解 运用了函数会自行调用 valueOf 方法这个技巧

add(1); 			// 1
add(1)(2);  	// 3
add(1)(2)(3)// 6
add(1)(2, 3); // 6
add(1, 2)(3); // 6
add(1, 2, 3); // 6

之前参阅 2 篇文章,可以参考一二。
1、【进阶 6-1 期】JavaScript 高阶函数浅析
2、【进阶 6-2 期】深入高阶函数应用之柯里化

其中第一篇文章给出了前三个功能的实现,并没有覆盖到后面三种。
第二篇文章实现了一个通用的柯里化函数,覆盖实现了所有功能。

解析:

去重数字组数 ) :使用高阶函数:

const arr1 = [1, 2, 1, 2, 3, 5, 4, 5, 3, 4, 4, 4, 4];
const arr2 = arr1.filter((element, index, self) => {
  return self.indexOf(element) === index;
});

console.log(arr2);
// [1, 2, 3, 5, 4]
console.log(arr1);
// [1, 2, 1, 2, 3, 5, 4, 5, 3, 4, 4, 4, 4]

函数作为返回值输出

let isType = (type) => (obj) => {
  return Object.prototype.toString.call(obj) === "[object " + type + "]";
};
isType("String")("123"); // true
isType("Array")([1, 2, 3]); // true
isType("Number")(123); // true

答案:

function add() {
  console.log("进入add");
  var args = Array.prototype.slice.call(arguments);

  var fn = function () {
    var arg_fn = Array.prototype.slice.call(arguments);
    console.log("调用fn");
    return add.apply(null, args.concat(arg_fn));
  };

  fn.valueOf = function () {
    console.log("调用valueOf");
    return args.reduce(function (a, b) {
      return a + b;
    });
  };

  return fn;
}
/*
    add(1);
    // 输出如下:
    // 进入add
    // 调用valueOf
    // 1

    add(1)(2);
    // 输出如下:
    // 进入add
    // 调用fn
    // 进入add
    // 调用valueOf
    // 3
    
    add(1)(2)(3);
    // 输出如下:
    // 进入add
    // 调用fn
    // 进入add
    // 调用fn
    // 进入add
    // 调用valueOf
    // 6
*/

这里有个规律,如果只改写 valueOf() 或是 toString() 其中一个,会优先调用被改写了的方法,而如果两个同时改写,则会像 String 转换规则一样,优先查询 valueOf() 方法,在 valueOf() 方法返回的是非原始类型的情况下再查询 toString() 方法。

# 第 85 题 (2019/11/23)

题目: react-router 里的 <Link> 标签和 <a> 标签有什么区别

如何禁掉 <a> 标签默认事件,禁掉之后如何实现跳转。

解析:

从最终渲染的 DOM 来看,这两者都是链接,都是 <a> 标签,区别是:
<Link> 是 react-router 里实现路由跳转的链接,一般配合 <Route> 使用,react-router 接管了其默认的链接跳转行为,区别于传统的页面跳转, <Link> 的 “跳转” 行为只会触发相匹配的 <Route> 对应的页面内容更新,而不会刷新整个页面。
<a> 标签就是普通的超链接了,用于从当前页面跳转到 href 指向的另一个页面(非锚点情况)。

# 第 86 题 (2019/11/23)

题目: 周一算法题之「两数之和」

给定一个整数数组和一个目标值,找出数组中和为目标值的两个数。

你可以假设每个输入只对应一种答案,且同样的元素不能被重复利用。

示例:

给定 nums = [2, 7, 11, 15], target = 9

因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]

公司 :京东、快手

function Getarr(num, target) {
  var pre = 0,
    cur = num.length - 1;
  if (num.length < 2) {
    return "至少提供2个数字";
  }
  while (pre < cur) {
    result = num[pre] + num[cur];
    if (result > target) {
      cur--;
    } else if (result < target) {
      pre++;
    } else {
      return [pre, cur];
    }
  }
}

# 第 87 题 (2019/11/24)

** 题目:** 在输入框中如何判断输入的是一个正确的网址。

解析: location 可以获取本页面的 URL 信息

不上正则,一个简单的玩法

function isUrl(url) {
	const a = document.createElement('a')
	a.href = url
	return [
		/^(http|https):$/.test(a.protocol), // "https:" 协议
		a.host,  						//  "baidu.com" => 端口(port)
		a.pathname !== url,				// "/"
		a.pathname !== `/${url}`,
	].find(x => !x) === undefined
}

利用 URL() 构造函数返回一个新创建的 URL 对象

function isUrl(url) {
  try {
    new URL(url);
    return true;
  } catch (err) {
    return false;
  }
}
const isUrl = (urlStr) => {
  try {
    const { href, origin, host, hostname, pathname } = new URL(urlStr);
    return href && origin && host && hostname && pathname && true;
  } catch (e) {
    return false;
  }
};

正则:

/^(https?:\/\/)?([a-z0-9]\.|[a-z0-9][-a-z0-9]*[a-z0-9]\.)*([a-z]+)(:\d+)?(\/.*)?$/;

# 第 88 题 (2019/12/04)

以下数据结构中,id 代表部门编号,name 是部门名称,parentId 是父部门编号,为 0 代表一级部门,现在要求实现一个 convert 方法,把原始 list 转换成树形结构,parentId 为多少就挂载在该 id 的属性 children 数组下,结构如下:

// 原始 list 如下
let list =[
    {id:1,name:'部门A',parentId:0},
    {id:2,name:'部门B',parentId:0},
    {id:3,name:'部门C',parentId:1},
    {id:4,name:'部门D',parentId:1},
    {id:5,name:'部门E',parentId:2},
    {id:6,name:'部门F',parentId:3},
    {id:7,name:'部门G',parentId:2},
    {id:8,name:'部门H',parentId:4}
];
const result = convert(list, ...);

// 转换后的结果如下
let result = [
    {
      id: 1,
      name: '部门A',
      parentId: 0,
      children: [
        {
          id: 3,
          name: '部门C',
          parentId: 1,
          children: [
            {
              id: 6,
              name: '部门F',
              parentId: 3
            }, {
              id: 16,
              name: '部门L',
              parentId: 3
            }
          ]
        },
        {
          id: 4,
          name: '部门D',
          parentId: 1,
          children: [
            {
              id: 8,
              name: '部门H',
              parentId: 4
            }
          ]
        }
      ]
    },
  ···
];

解析: ~~

# 第 89 题 (2019/12/04)

题目 :设计并实现 Promise.race ()

解析: 代写~

# 第 90 题 (2019/12/05)

题目: 实现模糊搜索结果的关键词高亮显示

mark

考虑节流、缓存。其实还可以上列表 diff + 定时清理缓存

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>auto complete</title>
    <style>
      bdi {
        color: rgb(0, 136, 255);
      }

      li {
        list-style: none;
      }
    </style>
  </head>
  <body>
    <input class="inp" type="text" />
    <section>
      <ul class="container"></ul>
    </section>
  </body>
  <script>
    function debounce(fn, timeout = 300) {
      let t;
      return (...args) => {
        if (t) {
          clearTimeout(t);
        }
        t = setTimeout(() => {
          fn.apply(fn, args);
        }, timeout);
      };
    }

    function memorize(fn) {
      const cache = new Map();
      return (name) => {
        if (!name) {
          container.innerHTML = "";
          return;
        }
        if (cache.get(name)) {
          container.innerHTML = cache.get(name);
          return;
        }
        const res = fn.call(fn, name).join("");
        cache.set(name, res);
        container.innerHTML = res;
      };
    }

    function handleInput(value) {
      const reg = new RegExp(`\(${value}\)`);
      const search = data.reduce((res, cur) => {
        if (reg.test(cur)) {
          const match = RegExp.$1;
          res.push(`<li>${cur.replace(match, "<bdi>$&</bdi>")}</li>`);
        }
        return res;
      }, []);
      return search;
    }

    const data = [
      "上海野生动物园",
      "上饶野生动物园",
      "北京巷子",
      "上海中心",
      "上海黄埔江",
      "迪士尼上海",
      "陆家嘴上海中心",
    ];

    const container = document.querySelector(".container");

    const memorizeInput = memorize(handleInput);

    document.querySelector(".inp").addEventListener(
      "input",
      debounce((e) => {
        console.log(e.target.value);
        memorizeInput(e.target.value);
      })
    );
  </script>
</html>

# 第 91 题 (2019/12/05)

题目: 介绍下 HTTPS 中间人攻击

解析:

https 协议由 http + ssl 协议构成,具体的链接过程可参考 SSL 或 TLS 握手的概述

中间人攻击过程如下:

  1. 服务器向客户端发送公钥。
  2. 攻击者截获公钥,保留在自己手上。
  3. 然后攻击者自己生成一个【伪造的】公钥,发给客户端。
  4. 客户端收到伪造的公钥后,生成加密 hash 值发给服务器。
  5. 攻击者获得加密 hash 值,用自己的私钥解密获得真秘钥。
  6. 同时生成假的加密 hash 值,发给服务器。
  7. 服务器用私钥解密获得假秘钥。
  8. 服务器用加秘钥加密传输信息

防范方法:

  1. 服务端在发送浏览器的公钥中加入 CA 证书,浏览器可以验证 CA 证书的有效性

# 第 92 题 (2019/12/15)

** 题目:** 已知数据格式,实现一个函数 fn 找出链条中所有的父级 id

const value = '112'
const fn = (value) => {
...
}
fn(value) // 输出 [1, 11, 112]

解析:

const value = "112";
const fn = (value) => {
  let arr = [];
  for (var i = 0; i < value.length; i++) {
    arr.push(value.slice(0, i + 1));
  }
  return arr.map(Number);
};

# 第 93 题 (2019/12/15)

题目: 给定两个大小为 m 和 n 的有序数组 nums1 和 nums2。请找出这两个有序数组的中位数。要求算法的时间复杂度为 O (log (m+n))。

示例 1:

nums1 = [1, 3];
nums2 = [2];

中位数是 2.0

示例 2:

nums1 = [1, 2];
nums2 = [3, 4];

中位数是 (2 + 3) / 2 = 2.5

解析:

# 第 94 题 (2019/12/15)

题目: vue 在 v-for 时给每项元素绑定事件需要用事件代理吗?为什么?

解析:

Well, delegation has two main advantages: one is practical - it saves you from having to add (and remove!!) those listeners individually. But Vue already does that for you.

The other one is performance / memory. But since every click listener in a v-vor loop would use the same callback, this is minimal unless you have hundreds or thousands of rows.

And finally, you can use delegation pretty easily by adding an @click listener to the

    element instead of the children. But then you have to resort to checks on the click target to evaluate which item in your data it might represent. So I would only use that if you truly find any performance problems without delegation.

好,委派有两个主要优点:一个是实用的 - 它使您不必分别添加(和删除!)这些侦听器。 但是 Vue 已经为您做到了。

另一个是性能 / 内存。 但是,由于 v-vor 循环中的每个单击侦听器都将使用相同的回调,因此除非您有成百上千的行,否则这是最小的。

最后,您可以通过在 <ul > 元素(而不是子元素)中添加 @click 侦听器来轻松使用委派。 但是随后,您必须求助于点击目标,以评估数据中可能代表的项目。 因此,只有在您真正发现任何性能问题而没有委派的情况下,我才使用它。

# 第 95 题 (2019/12/23)

题目: 模拟实现一个深拷贝,并考虑对象相互引用以及 Symbol 拷贝的情况

解析:

一个不考虑其他数据类型的公共方法,基本满足大部分场景

function deepCopy(target, cache = new Set()) {
  if (typeof target !== "object" || cache.has(target)) {
    return target;
  }
  if (Array.isArray(target)) {
    target.map((t) => {
      cache.add(t);
      return t;
    });
  } else {
    return [
      ...Object.keys(target),
      ...Object.getOwnPropertySymbols(target),
    ].reduce(
      (res, key) => {
        cache.add(target[key]);
        res[key] = deepCopy(target[key], cache);
        return res;
      },
      target.constructor !== Object
        ? Object.create(target.constructor.prototype)
        : {}
    );
  }
}

主要问题是

  1. symbol 作为 key,不会被遍历到,所以 stringify 和 parse 是不行的
  2. 有环引用,stringify 和 parse 也会报错

我们另外用 getOwnPropertySymbols 可以获取 symbol key 可以解决问题 1,用集合记忆曾经遍历过的对象可以解决问题 2。当然,还有很多数据类型要独立去拷贝。比如拷贝一个 RegExp,lodash 是最全的数据类型拷贝了,有空可以研究一下

另外,如果不考虑用 symbol 做 key,还有两种黑科技深拷贝,可以解决环引用的问题,比 stringify 和 parse 优雅强一些

function deepCopyByHistory(target) {
  const prev = history.state;
  history.replaceState(target, document.title);
  const res = history.state;
  history.replaceState(prev, document.title);
  return res;
}

async function deepCopyByMessageChannel(target) {
  return new Promise((resolve) => {
    const channel = new MessageChannel();
    channel.port2.onmessage = (ev) => resolve(ev.data);
    channel.port1.postMessage(target);
  }).then((data) => data);
}

无论哪种方法,它们都有一个共性:失去了继承关系,所以剩下的需要我们手动补上去了,故有 Object.create(target.constructor.prototype) 的操作

有两个问题:

  1. 如果 target 是一个数组,拷贝结果没有返回
  2. 如果 target 是一个函数,函数没有被深拷贝

【Step-By-Step】一周面试题深入解析

# 【Step-By-Step】一周面试题深入解析 / 周刊 01

已完结~

今天 2019/11/04 😜 (ง・_・)ง

# 1. 如何正确判断 this 的指向?(2019-09-19)

如果用一句话说明 this 的指向,那么即是:谁调用它,this 就指向谁。

但是仅通过这句话,我们很多时候并不能准确判断 this 的指向。因此我们需要借助一些规则去帮助自己:

this 的指向可以按照以下顺序判断:

1、全局环境中的 this

浏览器环境:无论是否在 严格模式 下,在全局执行环境中(在任何函数体外部)this 都指向全局对象 window ;

node 环境:无论是否在严格模式下,在全局执行环境中(在任何函数体外部),this 都是空对象 {}

2、是否是 new 绑定

如果是 new 绑定,并且构造函数中没有返回 function 或者是 object,那么 this 指向这个新对象。如下:

构造函数返回值不是 function 或 object。

function Super(age) {
  this.age = age;
}

let instance = new Super("26");
console.log(instance.age); //26

构造函数返回值是 function 或 object,这种情况下 this 指向的是返回的对象。

function Super(age) {
  this.age = age;
  let obj = { a: "2" };
  return obj;
}

let instance = new Super("hello");
console.log(instance.age); //undefined

你可以想知道为什么会这样?我们来看一下 new 的实现原理:

  1. 创建一个新对象。
  2. 这个新对象会被执行 [[原型]] 连接。
  3. 属性和方法被加入到 this 引用的对象中。并执行了构造函数中的方法.
  4. 如果函数没有返回其他对象,那么 this 指向这个新对象,否则 this 指向构造函数中返回的对象。
function new(func) {
  let target = {};
  target.__proto__ = func.prototype;
  let res = func.call(target);
  //排除 null 的情况
  if ((res && typeof res == "object") || typeof res == "function") {
    return res;
  }
  return target;
}

3、函数是否通过 call,apply 调用,或者使用了 bind 绑定,如果是,那么 this 绑定的就是指定的对象【归结为显式绑定】。

function info() {
  console.log(this.age);
}
var person = {
  age: 20,
  info,
};
var age = 28;
var info = person.info;
info.call(person); //20
info.apply(person); //20
info.bind(person)(); //20

这里同样需要注意一种特殊情况,如果 call,apply 或者 bind 传入的第一个参数值是 undefined 或者 null ,严格模式下 this 的值为传入的值 null /undefined。非严格模式下,实际应用的默认绑定规则,this 指向全局对象 (node 环境为 global,浏览器环境为 window)

function info() {
  //node环境中:非严格模式 global,严格模式为null
  //浏览器环境中:非严格模式 window,严格模式为null
  console.log(this);
  console.log(this.age);
}
var person = {
  age: 20,
  info,
};
var age = 28;
var info = person.info;
//严格模式抛出错误;
//非严格模式,node下输出undefined(因为全局的age不会挂在 global 上)
//非严格模式。浏览器环境下输出 28(因为全局的age会挂在 window 上)
info.call(null);

**4、** 隐式绑定,函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。典型的隐式调用为: xxx.fn()

function info() {
  console.log(this.age);
}
var person = {
  age: 20,
  info,
};
var age = 28;
person.info(); //20;执行的是隐式绑定

5、 默认绑定,在不能应用其它绑定规则时使用的默认规则,通常是独立函数调用。

非严格模式: node 环境,执行全局对象 global,浏览器环境,执行全局对象 window。

严格模式:执行 undefined

function info() {
  console.log(this.age);
}
var age = 28;
//严格模式;抛错
//非严格模式,node下输出 undefined(因为全局的age不会挂在 global 上)
//非严格模式。浏览器环境下输出 28(因为全局的age会挂在 window 上)
//严格模式抛出,因为 this 此时是 undefined
info();

6、 箭头函数的情况:

let obj = {
  age: 20,
  info: function () {
    return () => {
      console.log(this.age); //this继承的是外层上下文绑定的this
    };
  },
};

let person = { age: 28 };
let info = obj.info();
info(); //20

let info2 = obj.info.call(person);
info2(); //28

点击查看更多

# 2.JS 中原始类型有哪几种?null 是对象吗?原始数据类型和复杂数据类型有什么区别?(2019-09-20)

目前,JS 原始类型有六种,分别为:

  • Boolean
  • String
  • Number
  • Undefined
  • Null
  • Symbol (ES6 新增)

ES10 新增了一种基本数据类型:BigInt

复杂数据类型只有一种: Object

null 不是一个对象,尽管 typeof null 输出的是 object ,这是一个历史遗留问题,JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象, null 表示为全零,所以将它错误的判断为 object

基本数据类型和复杂数据类型的区别为:

1、内存的分配不同

  • 基本数据类型存储在栈中。
  • 复杂数据类型存储在堆中,栈中存储的变量,是指向堆中的引用地址。

2、访问机制不同

  • 基本数据类型是按值访问
  • 复杂数据类型按引用访问,JS 不允许直接访问保存在堆内存中的对象,在访问一个对象时,首先得到的是这个对象在堆内存中的地址,然后再按照这个地址去获得这个对象中的值。

3、复制变量时不同 (a=b)

  • 基本数据类型:a=b; 是将 b 中保存的原始值的副本赋值给新变量 a,a 和 b 完全独立,互不影响
  • 复杂数据类型:a=b; 将 b 保存的对象内存的引用地址赋值给了新变量 a;a 和 b 指向了同一个堆内存地址,其中一个值发生了改变,另一个也会改变。

4、参数传递的不同 (实参 / 形参)

函数传参都是按值传递 (栈中的存储的内容):基本数据类型,拷贝的是值;复杂数据类型,拷贝的是引用地址

点击查看更多

# 3. 说一说你对 HTML5 语义化的理解 (2019-09-21)

语义化意味着顾名思义,HTML5 的语义化指的是合理正确的使用语义化的标签来创建页面结构,如 header,footer,nav,从标签上即可以直观的知道这个标签的作用,而不是滥用 div。

语义化的优点有:

  • 代码结构清晰,易于阅读,利于开发和维护
  • 方便其他设备解析(如屏幕阅读器)根据语义渲染网页。
  • 有利于搜索引擎优化(SEO),搜索引擎爬虫会根据不同的标签来赋予不同的权重

# 4. 如何让 (a == 1 && a == 2 && a == 3) 的值为 true?(2019-09-22)

可参考

4.1 利用隐式转换规则

== 操作符在左右数据类型不一致时,会先进行隐式转换。

a == 1 && a == 2 && a == 3 的值意味着其不可能是基本数据类型。因为如果 a 是 null 或者是 undefined bool 类型,都不可能返回 true。

因此可以推测 a 是复杂数据类型,JS 中复杂数据类型只有 object ,回忆一下,Object 转换为原始类型会调用什么方法?

  • 如果部署了 [Symbol.toPrimitive] 接口,那么调用此接口,若返回的不是基本数据类型,抛出错误。
  • 如果没有部署 [Symbol.toPrimitive] 接口,那么根据要转换的类型,先调用 valueOf / toString
    1. 非 Date 类型对象, hintdefault 时,调用顺序为: valueOf >>> toString ,即 valueOf 返回的不是基本数据类型,才会继续调用 toString ,如果 toString 返回的还不是基本数据类型,那么抛出错误。
    2. 如果 hintstring (Date 对象的 hint 默认是 string) ,调用顺序为: toString >>> valueOf ,即 toString 返回的不是基本数据类型,才会继续调用 valueOf ,如果 valueOf 返回的还不是基本数据类型,那么抛出错误。
    3. 如果 hintnumber ,调用顺序为: valueOf >>> toString

那么对于这道题,只要 [Symbol.toPrimitive] 接口,第一次返回的值是 1,然后递增,即成功成立。

let a = {
  [Symbol.toPrimitive]: (function (hint) {
    let i = 1;
    //闭包的特性之一:i 不会被回收
    return function () {
      return i++;
    };
  })(),
};
console.log(a == 1 && a == 2 && a == 3); //true

调用 valueOf 接口的情况:

let a = {
  valueOf: (function () {
    let i = 1;
    //闭包的特性之一:i 不会被回收
    return function () {
      return i++;
    };
  })(),
};
console.log(a == 1 && a == 2 && a == 3); //true

另外,除了 i 自增的方法外,还可以利用 正则,如下

let a = {
  reg: /\d/g,
  valueOf() {
    return this.reg.exec(123)[0];
  },
};
console.log(a == 1 && a == 2 && a == 3); //true

4.2 利用数据劫持

使用 Object.defineProperty 定义的属性,在获取属性时,会调用 get 方法。利用这个特性,我们在 window 对象上定义 a 属性,如下:

let i = 1;
Object.defineProperty(window, "a", {
  get: function () {
    return i++;
  },
});
console.log(a == 1 && a == 2 && a == 3); //true

ES6 新增了 Proxy ,此处我们同样可以利用 Proxy 去实现,如下:

let a = new Proxy(
  {},
  {
    i: 1,
    get: function () {
      return () => this.i++;
    },
  }
);
console.log(a == 1 && a == 2 && a == 3); // true

4.3 数组的 toString 接口默认调用数组的 join 方法,重写数组的 join 方法。

let a = [1, 2, 3];
a.join = a.shift;
console.log(a == 1 && a == 2 && a == 3); //true

4.4 利用 with 关键字

let i = 0;

with ({
  get a() {
    return ++i;
  },
}) {
  console.log(a == 1 && a == 2 && a == 3); //true
}

# 5. 防抖 (debounce) 函数的作用是什么?有哪些应用场景,请实现一个防抖函数。(2019-09-23)

可参考第三题

# 防抖函数的作用

防抖函数的作用就是控制函数在一定时间内的执行次数。防抖意味着 N 秒内函数只会被执行一次,如果 N 秒内再次被触发,则重新计算延迟时间。

举例说明:小思最近在减肥,但是她非常贪吃。为此,与其男朋友约定好,如果 10 天不吃零食,就可以购买一个包 (不要问为什么是包,因为包治百病)。但是如果中间吃了一次零食,那么就要重新计算时间,直到小思坚持 10 天没有吃零食,才能购买一个包。所以,管不住嘴的小思,没有机会买包 (悲伤的故事)… 这就是防抖

不管吃没吃零食,每 10 天买一个包,中间想买包,忍着,等到第十天的时候再买,这种情况是节流。如何控制女朋友的消费,各位攻城狮们,get 到了吗?防抖可比节流有效多了!

# 防抖应用场景

  1. 搜索框输入查询,如果用户一直在输入中,没有必要不停地调用去请求服务端接口,等用户停止输入的时候,再调用,设置一个合适的时间间隔,有效减轻服务端压力。
  2. 表单验证
  3. 按钮提交事件。
  4. 浏览器窗口缩放,resize 事件等。

# 【Step-By-Step】一周面试题深入解析 / 周刊 02

本周面试题一览:

  • 节流 (throttle) 函数的作用是什么?有哪些应用场景,请实现一个节流函数
  • 说一说你对 JS 执行上下文栈和作用域链的理解?
  • 什么是 BFC?BFC 的布局规则是什么?如何创建 BFC?
  • let、const、var 的区别有哪些?
  • 深拷贝和浅拷贝的区别是什么?如何实现一个深拷贝?

# 6. 节流 (throttle) 函数的作用是什么?有哪些应用场景,请实现一个节流函数。(2019-09-24)

解析可参考第三题

节流函数的作用:

节流函数的作用是规定一个单位时间,在这个单位时间内最多只能触发一次函数执行,如果这个单位时间内多次触发函数,只能有一次生效。

举例说明:小明的妈妈和小明约定好,如果小明在周考中取得满分,那么当月可以带他去游乐场玩,但是一个月最多只能去一次。

这其实就是一个节流的例子,在一个月的时间内,去游乐场最多只能触发一次。即使这个时间周期内,小明取得多次满分。

节流应用场景:

1. 按钮点击事件

2. 拖拽事件

3.onScoll

4. 计算鼠标移动的距离 (mousemove)

# 7. 说一说你对 JS 执行上下文栈和作用域链的理解?(2019-09-24)

JS 执行上下文

执行上下文就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行。

执行上下文类型分为:

  • 全局执行上下文
  • 函数执行上下文
  • eval 函数执行上下文 (不被推荐)

执行上下文创建过程中,需要做以下几件事:

  1. 创建变量对象:首先初始化函数的参数 arguments,提升函数声明和变量声明。
  2. 创建作用域链(Scope Chain):在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。
  3. 确定 this 的值,即 ResolveThisBinding

作用域

作用域负责收集和维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。—— 摘录自《你不知道的 JavaScript》(上卷)

作用域有两种工作模型:词法作用域和动态作用域,JS 采用的是词法作用域工作模型,词法作用域意味着作用域是由书写代码时变量和函数声明的位置决定的。( witheval 能够修改词法作用域,但是不推荐使用,对此不做特别说明)

作用域分为:

  • 全局作用域
  • 函数作用域
  • 块级作用域

JS 执行上下文栈 (后面简称执行栈)

执行栈,也叫做调用栈,具有 LIFO (后进先出) 结构,用于存储在代码执行期间创建的所有执行上下文。

规则如下:

  • 首次运行 JavaScript 代码的时候,会创建一个全局执行的上下文并 Push 到当前的执行栈中,每当发生函数调用,引擎都会为该函数创建一个新的函数执行上下文并 Push 当前执行栈的栈顶。
  • 当栈顶的函数运行完成后,其对应的函数执行上下文将会从执行栈中 Pop 出,上下文的控制权将移动到当前执行栈的下一个执行上下文。

以一段代码具体说明:

function fun3() {
  console.log("fun3");
}

function fun2() {
  fun3();
}

function fun1() {
  fun2();
}

fun1();

Global Execution Context (即全局执行上下文) 首先入栈,过程如下:

mark

作用域链

作用域链就是从当前作用域开始一层一层向上寻找某个变量,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是作用域链。

# 8. 什么是 BFC?BFC 的布局规则是什么?如何创建 BFC?(2019-09-25)

什么是 BFC

BFC 是 Block Formatting Context 的缩写,即块格式化上下文。我们来看一下 CSS2.1 规范中对 BFC 的说明

浮动、绝对定位的元素、非块级盒子的块容器(如 inline-blocks、table-cells 和 table-captions),以及 overflow 的值不为 visible (该值已传播到视区时除外)为其内容建立新的块格式上下文。

BFC 布局规则

  • BFC 内,盒子依次垂直排列。
  • BFC 内,两个盒子的垂直距离由 margin 属性决定。属于同一个 BFC 的两个相邻 Box 的 margin 会发生重叠【符合合并原则的 margin 合并后是使用大的 margin】
  • BFC 内,每个盒子的左外边缘接触内部盒子的左边缘(对于从右到左的格式,右边缘接触)。即使在存在浮动的情况下也是如此。除非创建新的 BFC。
  • BFC 的区域不会与 float box 重叠。
  • BFC 就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。
  • 计算 BFC 的高度时,浮动元素也参与计算。

如何创建 BFC

  • 根元素
  • 浮动元素(float 属性不为 none)
  • position 为 absolute 或 relative
  • overflow 不为 visible 的块元素
  • display 为 inline-block, table-cell, table-caption

BFC 的应用

  1. 防止 margin 重叠

根据 BFC 规则,同一个 BFC 内的两个两个相邻 Box 的 margin 会发生重叠,因此我们可以在 div 外面再嵌套一层容器,并且触发该容器生成一个 BFC,这样 <div class="a"></div> 就会属于两个 BFC,自然也就不会再发生 margin 重叠

<style>
    .a{
        height: 100px;
        width: 100px;
        margin: 50px;
        background: pink;
    }
    .container{
        overflow: auto; /*触发生成BFC*/
    }
</style>
<body>
    <div class="container">
        <div class="a"></div>
    </div>
    <div class="a"></div>
</body>
  1. 清除内部浮动
<style>
    <style>
    .a{
        height: 100px;
        width: 100px;
        margin: 10px;
        background: pink;
        float: left;
    }
    .container{
        width: 120px;
        display: inline-block;/*触发生成BFC*/
        border: 2px solid black;
    }
</style>
</style>
<body>
    <div class="container">
        <div class="a"></div>
    </div>
</body>

container 的高度没有被撑开,如果我们希望 container 的高度能够包含浮动元素,那么可以创建一个新的 BFC,因为根据 BFC 的规则,计算 BFC 的高度时,浮动元素也参与计算。

  1. 自适应多栏布局
<style>
    body{
        width: 500px;
    }
    .a{
        height: 150px;
        width: 100px;
        background: pink;
        float: left;
    }
    .b{
        height: 200px;
        overflow: hidden; /*触发生成BFC*/
        background: blue;
    }
</style>
<body>
    <div class="a"></div>
    <div class="b"></div>
</body>

mark

加了 overflow: hidden; 触发生成 BFC

mark

# 9. let、const、var 的区别有哪些?(2019-09-26)

mark

  1. let/const 定义的变量不会出现变量提升,而 var 定义的变量会提升。
  2. 相同作用域中,let 和 const 不允许重复声明,var 允许重复声明。
  3. cosnt 声明变量时必须设置初始值
  4. const 声明一个只读的常量,这个常量不可改变
  5. let/const 声明的变量仅在块级作用域中有效。而 var 声明的变量在块级作用域外仍能访问到。
  6. 顶层作用域中 var 声明的变量挂在 window 上 (浏览器环境)
  7. let/const 有暂时性死区的问题,即 let/const 声明的变量,在定义之前都是不可用的。如果使用会抛出错误。

# 10. 深拷贝和浅拷贝的区别是什么?如何实现一个深拷贝?(2019-09-27)

深拷贝和浅拷贝是针对复杂数据类型来说的。

深拷贝

深拷贝复制变量值,对于非基本类型的变量,则递归至基本类型变量后,再复制。 深拷贝后的对象与原来的对象是完全隔离的,互不影响,对一个对象的修改并不会影响另一个对象。

浅拷贝

浅拷贝是会将对象的每个属性进行依次复制,但是当对象的属性值是引用类型时,实质复制的是其引用,当引用指向的值改变时也会跟着变化。

可以使用 for inObject.assign 、 扩展运算符 ...Array.prototype.slice()Array.prototype.concat() 等,例如:

let obj = {
  name: "Yvette",
  age: 18,
  hobbies: ["reading", "photography"],
};
let obj2 = Object.assign({}, obj);
let obj3 = { ...obj };

obj.name = "Jack";
obj.hobbies.push("coding");
console.log(obj); //{ name: 'Jack', age: 18,hobbies: [ 'reading', 'photography', 'coding' ] }
console.log(obj2); //{ name: 'Yvette', age: 18,hobbies: [ 'reading', 'photography', 'coding' ] }
console.log(obj3); //{ name: 'Yvette', age: 18,hobbies: [ 'reading', 'photography', 'coding' ] }

可以看出浅拷贝只最第一层属性进行了拷贝,当第一层的属性值是基本数据类型时,新的对象和原对象互不影响,但是如果第一层的属性值是复杂数据类型,那么新对象和原对象的属性值其指向的是同一块内存地址。来看一下使用 for in 实现浅拷贝。

let obj = {
  name: "Yvette",
  age: 18,
  hobbies: ["reading", "photography"],
};
let newObj = {};
for (let key in obj) {
  newObj[key] = obj[key];
  //这一步不需要多说吧,复杂数据类型栈中存的是对应的地址,因此赋值操作,相当于两个属性值指向同一个内存空间
}
console.log(newObj);
//{ name: 'Yvette', age: 18, hobbies: [ 'reading', 'photography' ] }
obj.age = 20;
obj.hobbies.pop();
console.log(newObj);
//{ name: 'Yvette', age: 18, hobbies: [ 'reading' ] }

深拷贝实现

深拷贝最简单的实现是: JSON.parse(JSON.stringify(obj))

let obj = {
  name: "Yvette",
  age: 18,
  hobbies: ["reading", "photography"],
};
let newObj = JSON.parse(JSON.stringify(obj)); //newObj和obj互不影响
obj.hobbies.push("coding");
console.log(newObj); //{ name: 'Yvette', age: 18, hobbies: [ 'reading', 'photography' ] }

JSON.parse(JSON.stringify(obj)) 是最简单的实现方式,但是有一点缺陷:

  1. 对象的属性值是函数时,无法拷贝。
let obj = {
  name: "Yvette",
  age: 18,
  hobbies: ["reading", "photography"],
  sayHi: function () {
    console.log(sayHi);
  },
};
let newObj = JSON.parse(JSON.stringify(obj));
console.log(newObj); //{ name: 'Yvette', age: 18, hobbies: [ 'reading', 'photography' ] }
  1. 原型链上的属性无法获取
function Super() {}
Super.prototype.location = "NanJing";
function Child(name, age, hobbies) {
  this.name = name;
  this.age = age;
}
Child.prototype = new Super();

let obj = new Child("Yvette", 18);
console.log(obj.location); //NanJing
let newObj = JSON.parse(JSON.stringify(obj));
console.log(newObj); //{ name: 'Yvette', age: 18}
console.log(newObj.location); //undefined;原型链上的属性无法获取

3. 不能正确的处理 Date 类型的数据

4. 不能处理 RegExp

5. 会忽略 symbol

6. 会忽略 undefined

2. 实现一个 deepClone 函数

  1. 如果是基本数据类型,直接返回
  2. 如果是 RegExp 或者 Date 类型,返回对应类型
  3. 如果是复杂数据类型,递归。
function deepClone(obj) {
  //递归拷贝
  if (obj instanceof RegExp) return new RegExp(obj);
  if (obj instanceof Date) return new Date(obj);
  if (obj === null || typeof obj !== "object") {
    //如果不是复杂数据类型,直接返回
    return obj;
  }
  /**
   * 如果obj是数组,那么 obj.constructor 是 [Function: Array]
   * 如果obj是对象,那么 obj.constructor 是 [Function: Object]
   */
  let t = new obj.constructor();
  for (let key in obj) {
    //如果 obj[key] 是复杂数据类型,递归
    if (obj.hasOwnProperty(key)) {
      //是否是自身的属性
      t[key] = deepClone(obj[key]);
    }
  }
  return t;
}

测试:

function Super() {}
Super.prototype.location = "NanJing";
function Child(name, age, hobbies) {
  this.name = name;
  this.age = age;
  this.hobbies = hobbies;
}
Child.prototype = new Super();

let obj = new Child("Yvette", 18, ["reading", "photography"]);
obj.sayHi = function () {
  console.log("hi");
};
console.log(obj.location); //NanJing
let newObj = deepClone(obj);
console.log(newObj); //
console.log(newObj.location); //NanJing 可以获取到原型链上的属性
newObj.sayHi(); //hi 函数属性拷贝正常

# 【Step-By-Step】一周面试题深入解析 / 周刊 03

本周面试题一览:

  • 什么是 XSS 攻击,XSS 攻击可以分为哪几类?我们如何防范 XSS 攻击?
  • 如何隐藏页面中的某个元素?
  • 浏览器事件代理机制的原理是什么?
  • setTimeout 倒计时为什么会出现误差?

# 11. 什么是 XSS 攻击,XSS 攻击可以分为哪几类?我们如何防范 XSS 攻击?(2019-09-28)

源地址

1. XSS 攻击

XSS (Cross-Site Scripting,跨站脚本攻击) 是一种代码注入攻击。攻击者在目标网站上注入恶意代码,当被攻击者登陆网站时就会执行这些恶意代码,这些脚本可以读取 cookie,session tokens,或者其它敏感的网站信息,对用户进行钓鱼欺诈,甚至发起蠕虫攻击等。

XSS 的本质是:恶意代码未经过滤,与网站正常的代码混在一起;浏览器无法分辨哪些脚本是可信的,导致恶意脚本被执行。由于直接在用户的终端执行,恶意代码能够直接获取用户的信息,利用这些信息冒充用户向网站发起攻击者定义的请求。

XSS 分类

根据攻击的来源,XSS 攻击可以分为存储型 (持久性)、反射型 (非持久型) 和 DOM 型三种。下面我们来详细了解一下这三种 XSS 攻击:

1.1 反射型 XSS

当用户点击一个恶意链接,或者提交一个表单,或者进入一个恶意网站时,注入脚本进入被攻击者的网站。Web 服务器将注入脚本,比如一个错误信息,搜索结果等,未进行过滤直接返回到用户的浏览器上。

反射型 XSS 的攻击步骤:

  1. 攻击者构造出特殊的 URL ,其中包含恶意代码。
  2. 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。
  3. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索、跳转等。由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。

POST 的内容也可以触发反射型 XSS,只不过其触发条件比较苛刻(需要构造表单提交页面,并引导用户点击),所以非常少见。

如果不希望被前端拿到 cookie,后端可以设置 httpOnly (不过这不是 XSS攻击 的解决方案,只能降低受损范围)

如何防范反射型 XSS 攻击

对字符串进行编码。

对 url 的查询参数进行转义后再输出到页面

app.get("/welcome", function (req, res) {
  //对查询参数进行编码,避免反射型 XSS攻击
  res.send(`${encodeURIComponent(req.query.type)}`);
});

1.2 DOM 型 XSS

DOM 型 XSS 攻击,实际上就是前端 JavaScript 代码不够严谨,把不可信的内容插入到了页面。在使用 .innerHTML.outerHTML.appendChilddocument.write() 等 API 时要特别小心,不要把不可信的数据作为 HTML 插到页面上,尽量使用 .innerText.textContent.setAttribute() 等。

DOM 型 XSS 的攻击步骤:

  1. 攻击者构造出特殊数据,其中包含恶意代码。
  2. 用户浏览器执行了恶意代码。
  3. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

如何防范 DOM 型 XSS 攻击

防范 DOM 型 XSS 攻击的核心就是对输入内容进行转义 (DOM 中的内联事件监听器和链接跳转都能把字符串作为代码运行,需要对其内容进行检查)。

1. 对于 url 链接 (例如图片的 src 属性),那么直接使用 encodeURIComponent 来转义。

2. 非 url ,我们可以这样进行编码:

function encodeHtml(str) {
  return str
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&apos;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}

DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞。

1.3 存储型 XSS

恶意脚本永久存储在目标服务器上。当浏览器请求数据时,脚本从服务器传回并执行,影响范围比反射型和 DOM 型 XSS 更大。存储型 XSS 攻击的原因仍然是没有做好数据过滤:前端提交数据至服务端时,没有做好过滤;服务端在接受到数据时,在存储之前,没有做过滤;前端从服务端请求到数据,没有过滤输出

存储型 XSS 的攻击步骤:

  1. 攻击者将恶意代码提交到目标网站的数据库中。
  2. 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。
  3. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作

这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。

如何防范存储型 XSS 攻击:

  1. 前端数据传递给服务器之前,先转义 / 过滤 (防范不了抓包修改数据的情况)
  2. 服务器接收到数据,在存储到数据库之前,进行转义 / 过滤
  3. 前端接收到服务器传递过来的数据,在展示到页面前,先进行转义 / 过滤

除了谨慎的转义,我们还需要其他一些手段来防范 XSS 攻击:

1.Content Security Policy

在服务端使用 HTTP 的 Content-Security-Policy 头部来指定策略,或者在前端设置 meta 标签。

例如下面的配置只允许加载同域下的资源:

Content-Security-Policy: default-src 'self'
<meta http-equiv="Content-Security-Policy" content="form-action 'self';" />

前端和服务端设置 CSP 的效果相同,但是 meta 无法使用 report

严格的 CSP 在 XSS 的防范中可以起到以下的作用:

  1. 禁止加载外域代码,防止复杂的攻击逻辑。
  2. 禁止外域提交,网站被攻击后,用户的数据不会泄露到外域。
  3. 禁止内联脚本执行(规则较严格,目前发现 GitHub 使用)。
  4. 禁止未授权的脚本执行(新特性,Google Map 移动版在使用)。
  5. 合理使用上报可以及时发现 XSS,利于尽快修复问题。

2. 输入内容长度控制

对于不受信任的输入,都应该限定一个合理的长度。虽然无法完全防止 XSS 发生,但可以增加 XSS 攻击的难度。

3. 输入内容限制

对于部分输入,可以限定不能包含特殊字符或者仅能输入数字等。

4. 其他安全措施

  • HTTP-only Cookie: 禁止 JavaScript 读取某些敏感 Cookie,攻击者完成 XSS 注入后也无法窃取此 Cookie。
  • 验证码:防止脚本冒充用户提交危险操作。

点击查看更多

# 12. 如何隐藏页面中的某个元素?(2019-09-29)

隐藏类型:

屏幕并不是唯一的输出机制,比如说屏幕上看不见的元素(隐藏的元素),其中一些依然能够被读屏软件阅读出来(因为读屏软件依赖于可访问性树来阐述)。为了消除它们之间的歧义,我们将其归为三大类:

  • 完全隐藏:元素从渲染树中消失,不占据空间。
  • 视觉上的隐藏:屏幕中不可见,占据空间。
  • 语义上的隐藏:读屏软件不可读,但正常占据空。

完全隐藏:

  1. display 属性 (不占据空间)

HTML5 新增属性,相当于 display: none

<div hidden></div>
  1. hidden 属性 (不占据空间)

视觉上的隐藏:

利用 position 和 盒模型 将元素移出可视区范围

  1. 设置 posoitionabsolutefixed ,� 通过设置 topleft 等值,将其移出可视区域。(可视区域不占位)

  2. 设置 positionrelative ,通过设置 topleft 等值,将其移出可视区域。(可视区域占位);如希望其在可视区域不占位置,需同时设置 height: 0 ;

  3. 设置 margin 值,将其移出可视区域范围(可视区域占位);如果希望其在可视区域不占位,需同时设置 height: 0 ;

利用 transfrom

  1. 缩放
transform: scale(0);
  1. 移动 translateX , translateY
transform: translateX(-99999px);
  1. 旋转 rotate
transform: rotateY(90deg);

设置其大小为 0

  1. 宽高为 0,字体大小为 0
height: 0;
width: 0;
font-size: 0;
  1. 宽高为 0,超出隐藏
height: 0;
width: 0;
overflow: hidden;

设置透明度为 0

visibility 属性

层级覆盖, z-index 属性

position: relative;
z-index: -999;

再设置一个层级较高的元素覆盖在此元素上。

clip-path 裁剪

clip-path: polygon(0 0, 0 0, 0 0, 0 0);

# 13. 浏览器事件代理机制的原理是什么?(2019-09-30)

事件代理机制的原理

事件代理又称为事件委托,在祖先级 DOM 元素绑定一个事件,当触发子孙级 DOM 元素的事件时,利用事件冒泡的原理来触发绑定在祖先级 DOM 的事件。因为事件会从目标元素一层层冒泡至 document 对象。

为什么要事件代理?

  1. 添加到页面上的事件数量会影响页面的运行性能,如果添加的事件过多,会导致网页的性能下降。采用事件代理的方式,可以大大减少注册事件的个数。
  2. 事件代理的当时,某个子孙元素是动态增加的,不需要再次对其进行事件绑定。
  3. 不用担心某个注册了事件的 DOM 元素被移除后,可能无法回收其事件处理程序,我们只要把事件处理程序委托给更高层级的元素,就可以避免此问题。
  4. 允许给一个事件注册多个监听。
  5. 提供了一种更精细的手段控制 listener 的触发阶段 (可以选择捕获或者是冒泡)。
  6. 对任何 DOM 元素都是有效的,而不仅仅是对 HTML 元素有效。

addEventListener :

addEventListener 接受 3 个参数,分别是要处理的事件名、实现了 EventListener 接口的对象或者是一个函数、一个对象 / 一个布尔值

target.addEventListener(type, listener[, options]);
target.addEventListener(type, listener[, useCapture]);

options (对象) | 可选

  • capture: Boolean 。true 表示在捕获阶段触发,false 表示在冒泡阶段触发。默认是 false。
  • once: Boolean 。true 表示 listener 在添加之后最多只调用一次,listener 会在其被调用之后自动移除。默认是 false。
  • passive: Boolean 。true 表示 listener 永远不会调用 preventDefault() 。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。默认是 false。

useCapture (Boolean) | 可选

useCapture 默认为 false。表示冒泡阶段调用事件处理程序,若设置为 true,表示在捕获阶段调用事件处理程序。

如将页面中的所有 click 事件都代理到 document 上:

document.addEventListener(
  "click",
  function (e) {
    console.log(e.target);
    /**
     * 捕获阶段调用调用事件处理程序,eventPhase是 1;
     * 处于目标,eventPhase是2
     * 冒泡阶段调用事件处理程序,eventPhase是 3;
     */
    console.log(e.eventPhase);
  },
  false
);

addEventListener 相对应的是 removeEventListener , 用于移除事件监听。

# 14. setTimeout 倒计时为什么会出现误差? (2019-10-08)

setTimeout 只能保证延时或间隔不小于设定的时间。因为它实际上只是将回调添加到了宏任务队列中,但是如果主线程上有任务还没有执行完成,它必须要等待。

如果你对前面这句话不是非常理解,那么有必要了解一下 JS 的运行机制。

JS 的运行机制

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在 "任务队列"(task queue)。

(3)一旦 "执行栈" 中的所有同步任务执行完毕,系统就会读取 "任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

setTimeout(()=>{callback();}, 1000) ,即表示在 1s 之后将 callback 放到宏任务队列中,当 1s 的时间到达时,如果主线程上有其它任务在执行,那么 callback 就必须要等待,另外 callback 的执行也需要时间,因此 setTimeout 的时间间隔是有误差的,它只能保证延时不小于设置的时间。

如何减少 setTimeout 的误差

我们只能减少执行多次的 setTimeout 的误差,例如倒计时功能。

倒计时的时间通常都是从服务端获取的。造成误差的原因:

1. 没有考虑误差时间(函数执行的时间 / 其它代码的阻塞)

2. 没有考虑浏览器的 “休眠”

完全消除 setTimeout 的误差是不可能的,但是我们减少 setTimeout 的误差。通过对下一次任务的调用时间进行修正,来减少误差。

let count = 0;
let countdown = 5000; //服务器返回的倒计时时间
let interval = 1000;
let startTime = new Date().getTime();
let timer = setTimeout(countDownStart, interval); //首次执行
//定时器测试
function countDownStart() {
  count++;
  const offset = new Date().getTime() - (startTime + count * 1000);
  const nextInterval = interval - offset; //修正后的延时时间
  if (nextInterval < 0) {
    nextInterval = 0;
  }
  countdown -= interval;
  console.log(
    "误差:" +
      offset +
      "ms,下一次执行:" +
      nextInterval +
      "ms后,离活动开始还有:" +
      countdown +
      "ms"
  );
  if (countdown <= 0) {
    clearTimeout(timer);
  } else {
    timer = setTimeout(countDownStart, nextInterval);
  }
}

如果当前页面是不可见的,那么倒计时会出现大于 100ms 的误差时间。因此在页面显示时,应该重新从服务端获取剩余时间进行倒计时。当然,为了更好的性能,当倒计时不可见 (Tab 页切换 / 倒计时内容不在可视区时),可以选择停止倒计时。

为此,我们可以监听 visibityChange 事件进行处理。

点击查看更多

# 【Step-By-Step】一周面试题深入解析 / 周刊 04

本周面试题一览:

# 15. 什么是闭包?闭包的作用是什么? (2019-10-09)

什么是闭包?

闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包最常用的方式就是在一个函数内部创建另一个函数。

闭包的作用:

  1. 能够访问函数定义时所在的词法作用域 (阻止其被回收)。
  2. 私有化变量
function base() {
  let x = 10; //私有变量
  return {
    getX: function () {
      return x;
    },
  };
}
let obj = base();
console.log(obj.getX()); //10
  1. 模拟块级作用域
var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = (function (j) {
    return function () {
      console.log(j);
    };
  })(i);
}
a[6](); // 6
  1. 创建模块
function coolModule() {
  let name = "Yvette";
  let age = 20;
  function sayName() {
    console.log(name);
  }
  function sayAge() {
    console.log(age);
  }
  return {
    sayName,
    sayAge,
  };
}
let info = coolModule();
info.sayName(); //'Yvette'

模块模式具有两个必备的条件 (来自《你不知道的 JavaScript》)

  • 必须有外部的封闭函数,该函数必须至少被调用一次 (每次调用都会创建一个新的模块实例)
  • 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。

闭包的缺点

闭包会导致函数的变量一直保存在内存中,过多的闭包可能会导致内存泄漏

# 16. 实现 Promise.all 方法 (2019-10-10)

在实现 Promise.all 方法之前,我们首先要知道 Promise.all 的功能和特点,因为在清楚了 Promise.all 功能和特点的情况下,我们才能进一步去写实现。

Promise.all 功能

Promise.all(iterable) 返回一个新的 Promise 实例。此实例在 iterable 参数内所有的 promisefulfilled 或者参数中不包含 promise 时,状态变成 fulfilled ;如果参数中 promise 有一个失败 rejected ,此实例回调失败,失败原因的是第一个失败 promise 的返回结果。

let p = Promise.all([p1, p2, p3]);

p 的状态由 p1,p2,p3 决定,分成以下;两种情况:

(1)只有 p1、p2、p3 的状态都变成 fulfilled ,p 的状态才会变成 fulfilled ,此时 p1、p2、p3 的返回值组成一个数组,传递给 p 的回调函数。

(2)只要 p1、p2、p3 之中有一个被 rejected ,p 的状态就变成 rejected ,此时第一个被 reject 的实例的返回值,会传递给 p 的回调函数。

Promise.all 的特点

Promise.all 的返回值是一个 promise 实例

  • 如果传入的参数为空的可迭代对象, Promise.all同步 返回一个已完成状态的 promise
  • 如果传入的参数中不包含任何 promise, Promise.all异步 返回一个已完成状态的 promise
  • 其它情况下, Promise.all 返回一个 处理中(pending) 状态的 promise .

Promise.all 返回的 promise 的状态

  • 如果传入的参数中的 promise 都变成完成状态, Promise.all 返回的 promise 异步地变为完成。
  • 如果传入的参数中,有一个 promise 失败, Promise.all 异步地将失败的那个结果给失败状态的回调函数,而不管其它 promise 是否完成
  • 在任何情况下, Promise.all 返回的 promise 的完成状态的结果都是一个数组

Promise.all 实现

Promise.all = function (promises) {
  return new Promise((resolve, reject) => {
    //Array.from 将可迭代对象转换成数组
    promises = Array.from(promises);
    if (promises.length === 0) {
      resolve([]);
    } else {
      let result = [];
      let index = 0;
      for (let i = 0; i < promises.length; i++) {
        //考虑到 i 可能是 thenable 对象也可能是普通值
        Promise.resolve(promises[i]).then(
          (data) => {
            result[i] = data;
            if (++index === promises.length) {
              //所有的 promises 状态都是 fulfilled,promise.all返回的实例才变成 fulfilled 态
              resolve(result);
            }
          },
          (err) => {
            reject(err);
            return;
          }
        );
      }
    }
  });
};

# 17. 异步加载 js 脚本的方法有哪些? (2019-10-11)

  1. <script> 标签中增加 async (html5) 或者 defer (html4) 属性,脚本就会异步加载。
<script src="../XXX.js" defer></script>

deferasync 的区别在于:

  • defer 要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),在 window.onload 之前执行;
  • async 一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。
  • 如果有多个 defer 脚本,会按照它们在页面出现的顺序加载
  • 多个 async 脚本不能保证加载顺序
  1. 动态创建 script 标签

动态创建的 script ,设置 src 并不会开始下载,而是要添加到文档中,JS 文件才会开始下载。

let script = document.createElement("script");
script.src = "XXX.js";
// 添加到html文件中才会开始下载
document.body.append(script);
  1. XHR 异步加载 JS
let xhr = new XMLHttpRequest();
xhr.open("get", "js/xxx.js", true);
xhr.send();
xhr.onreadystatechange = function () {
  if (xhr.readyState == 4 && xhr.status == 200) {
    eval(xhr.responseText);
  }
};

# 18. 请实现一个 flattenDeep 函数,把嵌套的数组扁平化 (2019-10-13)

解析:

  1. 利用 Array.prototype.flat

ES6 为数组实例新增了 flat 方法,用于将嵌套的数组 “拉平”,变成一维的数组。该方法返回一个新数组,对原数组没有影响。

flat 默认只会 “拉平” 一层,如果想要 “拉平” 多层的嵌套数组,需要给 flat 传递一个整数,表示想要拉平的层数。

function flattenDeep(arr, deepLength) {
  return arr.flat(deepLength);
}
console.log(flattenDeep([1, [2, [3, [4]], 5]], 3));
function flattenDeep(arr) {
  // return arr.join(',').split(',').map(Number);
  return arr.toString().split(",").map(Number);
}
console.log(flattenDeep([1, [2, [3, [4]], 5]]));
  1. 利用 reduce 和 concat:
function flattenDeep(arr) {
  return arr.reduce(
    (acc, val) =>
      Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val),
    []
  );
}
console.log(flattenDeep([1, [2, [3, [4]], 5]]));
  1. 使用 stack 无限反嵌套多层嵌套数组
function flattenDeep(input) {
  const stack = [...input];
  const res = [];
  while (stack.length) {
    // 使用 pop 从 stack 中取出并移除值
    const next = stack.pop();
    if (Array.isArray(next)) {
      // 使用 push 送回内层数组中的元素,不会改动原始输入 original input
      stack.push(...next);
    } else {
      res.push(next);
    }
  }
  // 使用 reverse 恢复原数组的顺序
  return res.reverse();
}
console.log(flattenDeep([1, [2, [3, [4]], 5]]));

# 19. 可迭代对象有什么特点 (2019-10-14)

ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,换个角度,也可以认为,一个数据结构只要具有 Symbol.iterator 属性 ( Symbol.iterator 方法对应的是遍历器生成函数,返回的是一个遍历器对象),那么就可以其认为是可迭代的。

可迭代对象的特点

  • 具有 Symbol.iterator 属性, Symbol.iterator() 返回的是一个遍历器对象
  • 可以使用 for ... of 进行循环
let array = [1, 2, 3, 4];
let iter = array[Symbol.iterator]();
console.log(iter.next()); //{ value: 1, done: false }
console.log(iter.next()); //{ value: 2, done: false }
console.log(iter.next()); //{ value: 3, done: false }

原生具有 Iterator 接口的数据结构:

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

# 自定义一个可迭代对象

上面我们说,一个对象只有具有正确的 Symbol.iterator 属性,那么其就是可迭代的,因此,我们可以通过给对象新增 Symbol.iterator 使其可迭代

let obj = {
  name: "Yvette",
  age: 18,
  job: "engineer",
  *[Symbol.iterator]() {
    const self = this;
    const keys = Object.keys(self);
    for (let index = 0; index < keys.length; index++) {
      yield self[keys[index]]; //yield表达式仅能使用在 Generator 函数中
    }
  },
};

for (var key of obj) {
  console.log(key); //Yvette 18 engineer
}

# 【Step-By-Step】高频面试题深入解析 / 周刊 05 本周面试题一览:

本周面试题一览:

#

  • 实现 Promise.race 方法
  • JSONP 原理及简单实现
  • 实现一个数组去重的方法
  • 清楚浮动的方法有哪些
  • 编写一个通用的柯里化函数 currying

# 20. 实现 Promise.race 方法(2019-10-16)

在实现 Promise.race 方法之前,我们首先要知道 Promise.race 的功能和特点,因为在清楚了 Promise.race 功能和特点的情况下,我们才能进一步去写实现。

Promise.race 功能

Promise.race(iterable) 返回一个 promise,一旦 iterable 中的一个 promise 状态是 fulfilled / rejected ,那么 Promise.race 返回的 promise 状态是 fulfilled / rejected .

let p = Promise.race([p1, p2, p3]);

只要 p1、p2、p3 之中有一个实例率先改变状态,p 的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 p 的回调函数。

Promise.race 的特点

Promise.race 的返回值是一个 promise 实例

  • 如果传入的参数为空的可迭代对象,那么 Promise.race 返回的 promise 永远是 pending
  • 如果传入的参数中不包含任何 promisePromise.race 会返回一个处理中(pending)的 promise
  • 如果 iterable 包含一个或多个非 promise 值或已经解决的 promise,则 Promise.race 将解析为 iterable 中找到的第一个值。

Promise.race 的实现

Promise.race = function (promises) {
  //promises传入的是可迭代对象(省略参数合法性判断)
  promises = Array.from(promises); //将可迭代对象转换为数组
  return new Promise((resolve, reject) => {
    if (promises.length === 0) {
      //空的可迭代对象;
      //用于在pending态
    } else {
      for (let i = 0; i < promises.length; i++) {
        Promise.resolve(promises[i])
          .then((data) => {
            resolve(data);
          })
          .catch((reason) => {
            reject(reason);
          });
      }
    }
  });
};

# 21. JSONP 原理及简单实现 (2019-10-17)

尽管浏览器有同源策略,但是 <script> 标签的 src 属性不会被同源策略所约束,可以获取任意服务器上的脚本并执行。 jsonp 通过插入 script 标签的方式来实现跨域,参数只能通过 url 传入,仅能支持 get 请求。

实现原理:

  • Step1: 创建 callback 方法
  • Step2: 插入 script 标签
  • Step3: 后台接受到请求,解析前端传过去的 callback 方法,返回该方法的调用,并且数据作为参数传入该方法
  • Step4: 前端执行服务端返回的方法调用

jsonp 源码实现

function jsonp({ url, params, callback }) {
  return new Promise((resolve, reject) => {
    //创建script标签
    let script = document.createElement("script");
    //将回调函数挂在 window 上
    window[callback] = function (data) {
      resolve(data);
      //代码执行后,删除插入的script标签
      document.body.removeChild(script);
    };
    //回调函数加在请求地址上
    params = { ...params, callback }; //wb=b&callback=show
    let arrs = [];
    for (let key in params) {
      arrs.push(`${key}=${params[key]}`);
    }
    script.src = `${url}?${arrs.join("&")}`;
    document.body.appendChild(script);
  });
}

使用:

function show(data) {
  console.log(data);
}
jsonp({
  url: "http://localhost:3000/show",
  params: {
    //code
  },
  callback: "show",
}).then((data) => {
  console.log(data);
});

服务端代码 (node):

//express启动一个后台服务
let express = require("express");
let app = express();

app.get("/show", (req, res) => {
  let { callback } = req.query; //获取传来的callback函数名,callback是key
  res.send(`${callback}('Hello!')`);
});
app.listen(3000);

# 22、实现一个数组去重的方法 (2019-10-18)

  1. 法 1: 利用 ES6 新增数据类型 Set :[…new Set(arry)
  2. 利用 indexOf includes
function uniq(arry) {
  var result = [];
  for (var i = 0; i < arry.length; i++) {
    if (result.indexOf(arry[i]) === -1) {
      //如 result 中没有 arry[i],则添加到数组中
      result.push(arry[i]);
    }
  }
  return result;
}
  1. 利用 reduce
function uniq(arry) {
  return arry.reduce(
    (prev, cur) => (prev.includes(cur) ? prev : [...prev, cur]),
    []
  );
}
  1. 利用 Map
function uniq(arry) {
  let map = new Map();
  let result = new Array();
  for (let i = 0; i < arry.length; i++) {
    if (map.has(arry[i])) {
      map.set(arry[i], true);
    } else {
      map.set(arry[i], false);
      result.push(arry[i]);
    }
  }
  return result;
}

# 23、清除浮动的方法有哪些? (2019-10-20)

解析:

总体来说就两点吧:

  1. 利用 clear 元素
  2. 利用 BFC 布局规则

# 24. 编写一个通用的柯里化函数 currying (2019-10-20)

在开始之前,我们首先需要搞清楚函数柯里化的概念。

curry 的这种用途可以理解为:参数复用。本质上是降低通用性,提高适用性。

函数柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

const currying = (fn, ...args) =>
  args.length < fn.length
    ? //参数长度不足时,重新柯里化该函数,等待接受新参数
      (...arguments) => currying(fn, ...args, ...arguments)
    : //参数长度满足时,执行函数
      fn(...args);
function sumFn(a, b, c) {
  return a + b + c;
}
var sum = currying(sumFn);
console.log(sum(2)(3)(5)); //10
console.log(sum(2, 3, 5)); //10
console.log(sum(2)(3, 5)); //10
console.log(sum(2, 3)(5)); //10

函数柯里化的主要作用:

  • 参数复用
  • 提前返回 – 返回接受余下的参数且返回结果的新函数
  • 延迟执行 – 返回新函数,等待执行

# 【Step-By-Step】高频面试题深入解析 / 周刊 06

本周面试题一览:

  • 原型链继承的基本思路是什么?有什么优缺点?
  • 借用构造函数和组合继承基本思路是什么?有什么优缺点?
  • 原型式继承的基本思路是什么?有什么优缺点?
  • 寄生式继承的基本思路是什么?有什么优缺点?
  • 寄生组合式继承的基本思路是什么?有什么优缺点?

本周是继承专题,在开始之前,需要先了解构造函数、原型和原型链的相关知识。

# 构造函数

构造函数和普通函数的区别仅在于调用它们的方式不同,任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数;任何函数,如果不通过 new 操作符来调用,那么它就是一个普通函数。

实例拥有 constructor(构造函数) 属性,该属性返回创建实例对象的构造函数。

function Person(name, age) {
  this.name = name;
  this.age = age;
}

var Yvette = new Person("刘小夕", 20);
console.log(Yvette.constructor === Person); //true

有一点需要说明的是,除了基本数据类型的 constructor 外 ( nullundefinedconstructor 属性), constructor 属性是可以被重写的。因此检测对象类型时, instanceof 操作符比 contsrutor 更可靠一些。

function Person(name) {
  this.name = name;
}
function SuperType() {}
var Yvette = new Person("刘小夕");
console.log(Yvette.constructor); //[Function: Person]
Yvette.constructor = SuperType;
console.log(Yvette.constructor); //[Function: SuperType]

# 原型

我们创建的每个函数都有 prototype 属性,这个属性指向函数的原型对象。原型对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

在默认情况下,所有原型对象都会自动获得一个 constructor 属性,这个属性包含一个指向 prototype 属性所在函数的指针。

当调用构造函数创建一个新实例后,该实例的内部将包含一个指针,指向构造函数的原型对象 (可以通过实例的 __proto__ 来访问构造函数的原型对象)。 参考

function f() {}
f.prototype.sayhi = function (name) {
  this.name = name;
};

var a = new f();
console.log(f.prototype); //f { sayhi: [Function] }
console.log(f.prototype.constructor); //[Function: f]
console.log(a.__proto__); //f { sayhi: [Function] }
console.log(f.__proto__); //[Function]

总结: 1. 对象有属性 proto, 指向该对象的构造函数的原型对象。 2. 方法除了有属性 proto, 还有属性 prototype,prototype 指向该方法的原型对象。

讲完啦,欢迎各种批评指正完善探讨,共同进步~

# 原型链

简单回顾一下构造函数、原型和实例的关系:

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个可以执行原型对象的内部指针 (可以通过 __proto 访问)。

假如我们让原型对象等于另一个类型的实例,那么此时原型对象包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。加入另一个原型又是另一个类型的实例,那么上述关系仍然成立,如此层层递进,就构成了实例与原型的链条,这就是原型链的基本概念

function SuperType() {
  this.type = "animal";
}
SuperType.prototype.getType = function () {
  console.log(this.type);
};
function SubType() {}
SubType.prototype = new SuperType();
SubType.prototype.sayHello = function () {
  console.log("hello");
};
function SimType(name) {
  this.name = name;
}
SimType.prototype = new SubType();
SimType.prototype.sayHi = function () {
  console.log("hi");
};
var instance = new SimType("刘小夕");
instance.getType(); //animal

调用 instance.getType() 会调用以下的搜索步骤:

  1. 搜索 instance 实例
  2. 搜索 SimType.prototype
  3. 搜索 SubType.prototype
  4. 搜索 SuperType.prototype ,找到了 getType 方法

在找不到属性或方法的情况下,搜索过程总是要一环一环地前行到原型链的末端才会停下来。

所有引用类型都继承了 Object ,这个继承也是通过原型链实现的。如果在 SuperType.prototype 还没有找到 getType ,就会到 Object.prototype 中找 (图中少画了一环)。

# 原型链继承

原型链继承的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。

SubType.prototype = new SuperType() ;

function SuperType() {
  this.name = "Yvette";
  this.colors = ["pink", "blue", "green"];
}
SuperType.prototype.getName = function () {
  return this.name;
};
function SubType() {
  this.age = 22;
}
SubType.prototype = new SuperType();

SubType.prototype.getAge = function () {
  return this.age;
};

// console.log(SubType.prototype.constructor) //[Function: SuperType]
SubType.prototype.constructor = SubType;

let instance1 = new SubType();
instance1.colors.push("yellow");
console.log(instance1.getName()); //'Yvette'
console.log(instance1.colors); //[ 'pink', 'blue', 'green', 'yellow' ]

let instance2 = new SubType();
console.log(instance2.colors); //[ 'pink', 'blue', 'green', 'yellow' ]

可以看出 colors 属性会被所有的实例共享 (instance1、instance2、…)。

缺点:

  1. 通过原型来实现继承时,原型会变成另一个类型的实例,原先的实例属性变成了现在的原型属性,该原型的引用类型属性会被所有的实例共享。
  2. 在创建子类型的实例时,没有办法在不影响所有对象实例的情况下给超类型的构造函数中传递参数。

# 借用构造函数

借用构造函数的技术,其基本思想为:

在子类型的构造函数中调用超类型构造函数。

function SuperType(name) {
  this.name = name;
  this.colors = ["pink", "blue", "green"];
}

function SubType(name) {
  SuperType.call(this, name);
}

let intance1 = new SubType("zc");
console.log(intance1.name); //zc

优点:

  1. 可以向超类传递参数
  2. 解决了原型中包含引用类型值被所有实例共享的问题

缺点:

  1. 方法都在构造函数中定义,函数复用无从谈起,另外超类型原型中定义的方法对于子类型而言都是不可见的。

# 组合继承

组合继承指的是将原型链和借用构造函数技术组合到一块,从而发挥二者之长的一种继承模式。基本思路:

使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承,既通过在原型上定义方法来实现了函数复用,又保证了每个实例都有自己的属性。

function SuperType(name) {
  this.name = name;
  this.colors = ["pink", "blue", "green"];
}

SuperType.prototype.sayName = function () {
  console.log(this.name);
};
// console.log(SuperType)

function SuberType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}

SuberType.prototype = new SuperType();
SuberType.prototype.constructor = SuberType;
SuberType.prototype.sayAge = function () {
  console.log(this.age);
};
let instance1 = new SuberType("Yvette", 20);
instance1.colors.push("yellow");
console.log(instance1.colors); //[ 'pink', 'blue', 'green', 'yellow' ]
instance1.sayName(); //Yvette

let instance2 = new SuberType("Jack", 22);
console.log(instance2.colors); //[ 'pink', 'blue', 'green' ]
instance2.sayName(); //Jack

缺点:

  • 无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。

优点:

  • 可以向超类传递参数
  • 每个实例都有自己的属性
  • 实现了函数复用

# 原型式继承

浅拷贝与深拷贝

原型继承的基本思想:

借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

object() 函数内部,先穿甲一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例,从本质上讲, object() 对传入的对象执行了一次浅拷贝。

ECMAScript5 通过新增 Object.create() 方法规范了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象 (可以覆盖原型对象上的同名属性),在传入一个参数的情况下, Object.create()object() 方法的行为相同。

var person = {
  name: "Yvette",
  hobbies: ["reading", "photography"],
};
var person1 = Object.create(person);
person1.name = "Jack";
person1.hobbies.push("coding");
var person2 = Object.create(person);
person2.name = "Echo";
person2.hobbies.push("running");
console.log(person.hobbies); //[ 'reading', 'photography', 'coding', 'running' ]
console.log(person1.hobbies); //[ 'reading', 'photography', 'coding', 'running' ]

在没有必要创建构造函数,仅让一个对象与另一个对象保持相似的情况下,原型式继承是可以胜任的。

缺点:

同原型链实现继承一样,包含引用类型值的属性会被所有实例共享。

# 寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部已某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

function createAnother(original) {
  var clone = object(original); //通过调用函数创建一个新对象
  clone.sayHi = function () {
    //以某种方式增强这个对象
    console.log("hi");
  };
  return clone; //返回这个对象
}
var person = {
  name: "Yvette",
  hobbies: ["reading", "photography"],
};

var person2 = createAnother(person);
person2.sayHi(); //hi

基于 person 返回了一个新对象 -—— person2 ,新对象不仅具有 person 的所有属性和方法,而且还有自己的 sayHi() 方法。在考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。

缺点:

  • 使用寄生式继承来为对象添加函数,会由于不能做到函数复用而效率低下。
  • 同原型链实现继承一样,包含引用类型值的属性会被所有实例共享。

# 寄生组合式继承

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法,基本思路:

不必为了指定子类型的原型而调用超类型的构造函数,我们需要的仅是超类型原型的一个副本,本质上就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。寄生组合式继承的基本模式如下所示:

function inheritPrototype(subType, superType) {
  var prototype = object(superType.prototype); //创建对象
  prototype.constructor = subType; //增强对象
  subType.prototype = prototype; //指定对象
}
  • 第一步:创建超类型原型的一个副本
  • 第二步:为创建的副本添加 constructor 属性
  • 第三步:将新创建的对象赋值给子类型的原型

至此,我们就可以通过调用 inheritPrototype 来替换为子类型原型赋值的语句:

function SuperType(name) {
  this.name = name;
  this.colors = ["pink", "blue", "green"];
}
//...code
function SuberType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}
inheritPrototype(SuberType, SuperType);
//...code

优点:

只调用了一次超类构造函数,效率更高。避免在 SuberType.prototype 上面创建不必要的、多余的属性,与其同时,原型链还能保持不变。

因此寄生组合继承是引用类型最理性的继承范式。

# ES6 继承

Class 可以通过 extends 关键字实现继承,如:

class SuperType {
  constructor(age) {
    this.age = age;
  }

  getAge() {
    console.log(this.age);
  }
}

class SubType extends SuperType {
  constructor(age, name) {
    super(age); // 调用父类的constructor(x, y)
    this.name = name;
  }

  getName() {
    console.log(this.name);
  }
}

let instance = new SubType(22, "刘小夕");
instance.getAge(); //22

对于 ES6 的 class 需要做以下几点说明:

  1. 类的数据类型就是函数,类本身就指向构造函数。
console.log(typeof SuperType); //function
console.log(SuperType === SuperType.prototype.constructor); //true
  1. 类的内部所有定义的方法,都是不可枚举的。(ES5 原型上的方法默认是可枚举的)
Object.keys(SuperType.prototype);
  1. constructor 方法是类的默认方法,通过 new 命令生成对象实例时,自动调用该方法。一个类必须有 constructor 方法,如果没有显式定义,一个空的 constructor 方法会被默认添加。
  2. Class 不能像构造函数那样直接调用,会抛出错误。

使用 extends 关键字实现继承,有一点需要特别说明:

  • 子类必须在 constructor 中调用 super 方法,否则新建实例时会报错。如果没有子类没有定义 constructor 方法,那么这个方法会被默认添加。在子类的构造函数中,只有调用 super 之后,才能使用 this 关键字,否则报错。这是因为子类实例的构建,基于父类实例,只有 super 方法才能调用父类实例。
class SubType extends SuperType {
  constructor(...args) {
    super(...args);
  }
}

# 【Step-By-Step】高频面试题深入解析 / 周刊 07

本周面试题一览:

# 1. 实现一个 JSON.stringify (2019-10-27)

JSON.stringify([, replacer [, space]) 方法是将一个 JavaScript 值 (对象或者数组) 转换为一个 JSON 字符串。此处模拟实现,不考虑可选的第二个参数 replacer 和第三个参数 space ,如果对这两个参数的作用还不了解,建议阅读 MDN 文档。

JSON.stringify() 将值转换成对应的 JSON 格式:

1、基本数据类型:

  • undefined 转换之后仍是 undefined (类型也是 undefined )
  • boolean 值转换之后是字符串 "false"/"true"
  • number 类型 (除了 NaNInfinity ) 转换之后是字符串类型的数值
  • symbol 转换之后是 undefined
  • null 转换之后是字符串 "null"
  • string 转换之后仍是 string
  • NaNInfinity 转换之后是字符串 "null"

2、如果是函数类型

  • 转换之后是 undefined

3、如果是对象类型 (非函数)

  • 如果有 toJSON() 方法,那么序列化 toJSON() 的返回值。
  • 如果是一个数组
    • 如果属性值中出现了 undefined 、任意的函数以及 symbol ,转换成字符串 "null"
  • 如果是 RegExp 对象。
    返回 {} (类型是 string)
  • 如果是 Date 对象,返回 DatetoJSON 字符串值
  • 如果是普通对象;
    • 如果属性值中出现了 undefined 、任意的函数以及 symbol 值,忽略。
    • 所有以 symbol 为属性键的属性都会被完全忽略掉。

4、对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。

模拟实现

function jsonStringify(data) {
  let dataType = typeof data;
  if (dataType !== "object") {
    let result = data;
    //data 可能是 string/number/null/undefined/boolean
    if (Number.isNaN(data) || data === Infinity) {
      //NaN 和 Infinity 序列化返回 "null"
      result = "null";
    } else if (
      dataType === "function" ||
      dataType === "undefined" ||
      dataType === "symbol"
    ) {
      //function 、undefined 、symbol 序列化返回 undefined
      return undefined;
    } else if (dataType === "string") {
      result = '"' + data + '"';
    }
    //boolean 返回 String()
    return String(result);
  } else if (dataType === "object") {
    if (data === null) {
      return "null";
    } else if (data.toJSON && typeof data.toJSON === "function") {
      return jsonStringify(data.toJSON());
    } else if (data instanceof Array) {
      let result = [];
      //如果是数组
      //toJSON 方法可以存在于原型链中
      data.forEach((item, index) => {
        if (
          typeof item === "undefined" ||
          typeof item === "function" ||
          typeof item === "symbol"
        ) {
          result[index] = "null";
        } else {
          result[index] = jsonStringify(item);
        }
      });
      result = "[" + result + "]";
      return result.replace(/'/g, '"');
    } else {
      //普通对象
      /**
       * 循环引用抛错(暂未检测,循环引用时,堆栈溢出)
       * symbol key 忽略
       * undefined、函数、symbol 为属性值,被忽略
       */
      let result = [];
      Object.keys(data).forEach((item, index) => {
        if (typeof item !== "symbol") {
          //key 如果是symbol对象,忽略
          if (
            data[item] !== undefined &&
            typeof data[item] !== "function" &&
            typeof data[item] !== "symbol"
          ) {
            //键值如果是 undefined、函数、symbol 为属性值,忽略
            result.push('"' + item + '"' + ":" + jsonStringify(data[item]));
          }
        }
      });
      return ("{" + result + "}").replace(/'/g, '"');
    }
  }
}

# 2. 实现一个 JSON.parse (2019-10-28)

JSON.parse(JSON.parse(text[, reviver]) 方法用来解析 JSON 字符串,构造由字符串描述的 JavaScript 值或对象。提供可选的 reviver 函数用以在返回之前对所得到的对象执行变换。此处模拟实现,不考虑可选的第二个参数 reviver ,如果对这个参数的作用还不了解,建议阅读 MDN 文档。

第一种方式 eval

最简单,最直观的方式就是调用 eval

var json = '{"name":"小姐姐", "age":20}';
var obj = eval("(" + json + ")"); // obj 就是 json 反序列化之后得到的对象

直接调用 eval 存在 XSS 漏洞,数据中可能不是 json 数据,而是可执行的 JavaScript 代码。因此,在调用 eval 之前,需要对数据进行校验。

var rx_one = /^[\],:{}\s]*$/;
var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
var rx_four = /(?:^|:|,)(?:\s*\[)+/g;

if (
  rx_one.test(
    json.replace(rx_two, "@").replace(rx_three, "]").replace(rx_four, "")
  )
) {
  var obj = eval("(" + json + ")");
}

JSON 是 JS 的子集,可以直接交给 eval 运行。

第二种方式 new Function

Functioneval 有相同的字符串参数特性。

var json = '{"name":"小姐姐", "age":20}';
var obj = new Function("return " + json)();

# 3. 实现一个观察者模式 (2019-10-29)

观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新。观察者模式属于行为型模式,行为型模式关注的是对象之间的通讯,观察者模式就是观察者和被观察者之间的通讯。

观察者(Observer)直接订阅(Subscribe)主题(Subject),而当主题被激活的时候,会触发(Fire Event)观察者里的事件。

//有一家猎人工会,其中每个猎人都具有发布任务(publish),订阅任务(subscribe)的功能
//他们都有一个订阅列表来记录谁订阅了自己
//定义一个猎人类
//包括姓名,级别,订阅列表
function Hunter(name, level) {
  this.name = name;
  this.level = level;
  this.list = [];
}

Hunter.prototype.publish = function (money) {
  console.log(this.level + "猎人" + this.name + "寻求帮助");
  this.list.forEach(function (item, index) {
    item(money);
  });
};

Hunter.prototype.subscribe = function (targrt, fn) {
  console.log(this.level + "猎人" + this.name + "订阅了" + targrt.name);
  targrt.list.push(fn);
};

//猎人工会走来了几个猎人
let hunterMing = new Hunter("小明", "黄金");
let hunterJin = new Hunter("小金", "白银");
let hunterZhang = new Hunter("小张", "黄金");
let hunterPeter = new Hunter("Peter", "青铜");

//Peter等级较低,可能需要帮助,所以小明,小金,小张都订阅了Peter
hunterMing.subscribe(hunterPeter, function (money) {
  console.log(
    "小明表示:" + (money > 200 ? "" : "暂时很忙,不能") + "给予帮助"
  );
});
hunterJin.subscribe(hunterPeter, function () {
  console.log("小金表示:给予帮助");
});
hunterZhang.subscribe(hunterPeter, function () {
  console.log("小张表示:给予帮助");
});

//Peter遇到困难,赏金198寻求帮助
hunterPeter.publish(198);

//猎人们(观察者)关联他们感兴趣的猎人(目标对象),如Peter,当Peter有困难时,会自动通知给他们(观察者)

# 5. ES6 模块和 CommonJS 模块有哪些差异?(2019-10-30)

参考

  1. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  • ES6 模块在编译时,就能确定模块的依赖关系,以及输入和输出的变量。ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
  • CommonJS 加载的是一个对象,该对象只有在脚本运行完才会生成。
  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- `CommonJS` 输出的是一个值的拷贝(注意基本数据类型/复杂数据类型)

- ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
  1. ES6 模块自动采用严格模式,无论模块头部是否写了 "use strict";
  2. require 可以做动态加载, import 语句做不到, import 语句必须位于顶层作用域中。
  3. ES6 模块的输入变量是只读的,不能对其进行重新赋值
  4. 当使用 require 命令加载某个模块时,就会运行整个模块的代码。
  5. 当使用 require 命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
  • Copyrights © 2015-2021 zhou chen
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信