前端面试题自检 JS CSS 部分

劳伦斯
• 阅读 1544

JS

类型

JavaScript的简单数据类型

Number , String , Boolean , Undefined , Null , Symbol

typeof 操作符的返回值

  • number
  • string
  • boolean
  • undefined
  • object
  • function
  • symbol

typeof NaN 返回 number

typeof null 时也返回 object ,此为历史遗留问题

为什么数字和字符串可以使用方法?

因为数字和字符串在使用方法时会转换为包装对象

包装对象就是其对应的构造函数创造出来的

(1).toString() // (new Number(1)).toString() 

new Number(1) { __proto__: Number , [[PrimitiveValue]] : 1 }

包装对象上会有一个内部值 [[PrimitiveValue]],值为被包装的原始值

这个对象在表达式结束后就会被销毁,所以无法给字符串/数字上添加属性

0.1 + 0.2 === 0.3 // false ?

0.1 + 0.2 != 0.3背后的原理

  • JS 采用 IEEE 754双精度64位存储数据,所以并非JS独有这个问题,采用了这个规范的语言都有

  • 64位 :1位符号位,11位指数位,52位尾数位

    JS能表示最大的整数是 2^53 - 1(52个二进制 1 ),而不是 2^52 -1

  • 0.1 和 0.2 在二进制中表现为无限循环,所以需要在尾数位末尾处进行舍入,被称为精度丢失

  • 两个数相加之后就得到了十进制小数位末位为4而不为0的结果

解决方法

  • toFixed 可以精确到某一位,舍弃小数位

  • Number.EPSILON ['epsɪlɒn] 是 JS 能表示最小精度 2^(-52)

    const isEquel = (a, b) => Math.abs(a - b) < Number.EPSILON // 相等 
  • 转换成整数运算

    /**
     * 精确加法
     */
    function add(num1, num2) {
      const num1Digits = (num1.toString().split('.')[1] || '').length;
      const num2Digits = (num2.toString().split('.')[1] || '').length;
      const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
      return (num1 * baseNum + num2 * baseNum) / baseNum;
    }
    add(0.1,0.2); // 0.3 

类型转化与规则

转到boolean

除了以下 5 种,其他都被转为 true

  • undefined
  • null
  • 0(包括+0和-0)
  • NaN
  • "" 空字符串

toString

toString(10,2) //  "1010" 输入数字时,第二个参数可选为进制

toString(new Date())  // Wed Jan 20 2021 20:06:24 GMT+0800 (中国标准时间)

toString([1,2]) // "1,2" 

很多内置类型的原型上都被重写了toString方法

判断类型时,可以调用Object上的toString方法如数组 ({}).toString.call(x) === '[object Array]'

valueOf

  • null 和 undefined 没有包装对象 不能调用valueOf
  • Number Boolean String 的 prototype 上各自实现了一个 valueOf,不过功能一样,即返回包装对象内部的[[primitiveValue]]值
  • 各种内置对象类型,除了 Date 原型上实现了 valueOf,返回的是形如:1536416960724 的从 1970 年 1 月 1 日午夜开始计的毫秒数 ;其他都未自定义即 Object.prototype.valueOf,返回调用者自身。

toPrimitive

用于转换对象到原始值的内置函数,通过Symbol.toPrimitive可以覆写一个对象的转换到原始值的行为

它有个参数 hint,一般有两个可传入值 "number" "string",内部根据上下文选择传入或不传入

  • "number":先调用 valueOf 再 toString
  • "string":先调用 toString 再 valueOf
  • 非Date对象默认传入"number",Date对象默认传入"string";不过实际上都是调用了 toString

关于对象更细节的转化,请了解 toPrimitive ECMAScript7规范中的ToPrimitive抽象操作

转到number

  • boolean,true 转 1 false 转 0

  • null 转 0,undefined 转 NaN

  • string

    • 如果字符串只包含数字(包括十六进制格式“0x”),则将其转换成对应的十进制。
    • 如果字符串是空,"""\n",返回0。
    • 其他情况转为 NAN
  • 对象,调用 ToPrimitive 方法,PreferredType 参数为 "number",即

    ​ 1. 调用 valueOf 方法

    ​ 2.调用 toString 方法

    ​ 3. 转到 string 的情况

对象转换为数字实例

Number([1]) // 1

​ 1. [1].valueOf() 返回 [1]

​ 2. [1].toSting() 返回 "1"

​ 3. "1" 转为 1

Number([1,1]) // NaN

​ 1. [1,1].valueOf() 返回 [1,1]

​ 2. [1,,1].toSting() 返回 "1,1"

​ 3. "1,1" 转为 NaN

隐式转换和valueOf、toString

js将对象转换为基本类型时,会调用内部函数 ToPrimitive 进行转换,

分为以下两点

  • 非 Date 类型先 valueOftoString
  • Date 类型先 toStringvalueOf

考虑到这两种类型实际都是调用了 toStringvalueOf 并未改变输出 (除包装类型外),

所以默认对象的隐式转换都是调用了 toString

[] == false // true []先调用 toString转化为"",""取布尔是false
![] // false
{} + 1 // [object Object]1 

符号中的强制类型隐式转换

a + b 的转换

表中 object 是非包装对象(下表可以不看,看总结即可)

左右值组合\符号 +
string number number -> string
string boolean boolean -> string
string object object -> object.toString()
number boolean boolean -> number
number object number -> string,object -> object.toString()
boolean boolean boolean -> number
boolean object boolean -> string,object -> object.toString()
object object object -> object.toString()
null number null -> 0
null object null -> "null",object -> object.toString()
null boolean null -> 0,boolean -> number
null string null -> "null"
null null 0
undefined number undefined -> NaN
undefined object undefined -> "undefined",object -> object.toString()
undefined boolean undefined -> NaN,boolean -> number
undefined string undefined -> "undefined"
undefined undefined NaN
undefined null NaN

注意 undefined 到 number 转换为 NaN,而 null 转换为 0

通过上表的排列组合我们看出:

1.一边有对象(非包装)先转为字符串(实际上是调用 toPrimitive)

特别注意 Date 对象,它也是转为字符串

 new Date() + 1 // "Fri Mar 19 2021 10:59:08 GMT+0800 (中国标准时间)1" 

2.一边是字符串,另一边也转为字符串

1 + "1" = "11"

3.一边是布尔值,另一边是数字或布尔值,布尔值转为数字

4.一边是 null 或 undefined,另一边是 字符串 或者 数字,跟着另一边转;若都不是,null 或 undefined 先转为数字

undefined + true // NaN

null + true //  1 

5.不断从上往下检索规则,直到两边都是字符串或者数字。

注意 +ab + a,对 a的转换是不一样的:+a 是转换到 number,而 b + a 需要按照上述规则进行转换后相加

a == b 的转换

对象(非包装)比较或者不同类型比较时:

​ 1.两边是对象对比地址
​ 2.一边是对象先调用 toString(实际是 toPrimitive)
​ 3.一边是布尔值转换为数字
​ 4.两边分别是数字和字符串时,字符串转换为数字
​ 5.不断从上往下检索规则,直到两边类型相同。

其他情况

null == undefined // true  undefined 和 null 不与其他假值 `==`

NaN == NaN // false 需要判断 NaN,应该用 Number.isNaN 

实例

  • 'true' == true // false

    1. 符合 3,true 转为 1
    2. 符合 4,"true" 转为 NaN
    3. NaN == 1 返回false
  • [] == ![] // true

    1. ![] 转化为 false:除了 0,"",NaN,undefined,null,其他转布尔时都转为true,再取反为false

    2. 符合 2 和 3,[] 转为 "",false 转为 0

    3. 符合 4 ,"" 转为 0

    4. 0 == 0 返回 true

语言内置

关于Symbol

ES6入门教程#Symbol

关于Symbol,使用得不多,它的作用是作为一个唯一值,作对象的键,防止属性被覆盖或覆盖已存在属性

如手写 apply,为了防止覆盖传入的函数上的属性,我们可以用Symbol作为键

function myApply(ctx,args = []){
    if(typeof this !== 'function') {
        throw new TypeError('not a function!')
    }
    const symbol = Symbol(0)
    ctx[symbol] = this
    const result = ctx[symbol](...args)
    delete ctx[symbol]
    return result
}
Fuction.prototype.apply = myApply 

它第二个作用,它提供我们访问内置方法和覆写内置行为的可能。

Symbol上储存着各种内置方法的键,

通过重写类上的迭代器,可以改变实例使用迭代器的行为如

class Collection {
  *[Symbol.iterator]() {
    let i = 0;
    while(this[i] !== undefined) {
      yield this[i];
      ++i;
    }
  }
}

let myCollection = new Collection();
myCollection[0] = 1;
myCollection[1] = 2;

for(let value of myCollection) {
  console.log(value);
} 

迭代器(iterator)

迭代器是一个对象,它符合以下规范

  • 对象上可访问 next 函数

  • next 函数 返回 {value,done},value为本轮迭代的值,done为布尔值,表示迭代是否结束

  • 迭代器对象可以通过重复调用next()显式地迭代。 迭代一个迭代器被称为消耗了这个迭代器,因为它通常只能执行一次。 在产生终止值之后,对next()的额外调用应该继续返回{done:true}。

    var it = makeIterator(['a', 'b']);
    
    it.next() // { value: "a", done: false }
    it.next() // { value: "b", done: false } 最后一个值done为false,下一轮再next done为true
    it.next() // { value: undefined, done: true }
    
    function makeIterator(array) {
      var nextIndex = 0;
      return {
        next: function() {
          return nextIndex < array.length ?
            {value: array[nextIndex++], done: false} :
            {value: undefined, done: true};
        }
      };
    } 

迭代器接口(iterator)

ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”。

它是一个返回迭代器的函数。

我们可以通过 Symbol.interator 访问

let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();

iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true } 

原生具备 Iterator 接口的数据结构如下。

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

生成器函数(generator)

生成器因为出现不久后就被 async 函数取代了,我学习时已经遍布 async 语法了。

不过我们还是很有必要了解它的基础语法,因为它与迭代器有关,进而与 for of,解构语法等有关;

同时 async 是 generator 函数的语法糖,了解了 gennerator 的原理后 async 的原理也就很好理解了。

Generator 函数的语法

生成器和迭代器[MDN]

生成器生成什么?生成迭代器

生成器函数(generator)function * name{} 是一个返回迭代器的函数,这个迭代器可以自动维护自己的状态。

基本用法

  • function 关键词后添加 * ,声明生成器函数,调用生成器函数后,返回一个迭代器;

  • yield是迭代器调用next 后的执行暂停处,继续调用next执行到下一个next

  • yield后表达式的结果作为 next的返回值;

  • next传入的参数作为上一个 next暂停处整个yield 表达式的结果

  • 生成器函数最后 return 没有 yield 的效果,但是它会被保留在后续第一次调用next返回对象的 value

function* f() {
 for (let i=0; i<3; i++){
   if(yield i) yield 10 // 返回并记录函数状态
 }
 return 20
}
const iter = f()
iter.next() //第三条 {value:0,done:false}
iter.next() // {value:1,done:false}
iter.next(true) //第四条 {value:10,done:false} 上一个 yield 是 if(yield i) 传入true,if成功,执行到 yield 10
iter.next(true) // {value:2,done:false} 上一个 yield 是 yield 10 传入true 无影响
iter.next() //第五条 {value:20,done:true} 返回值 20 被保存了
iter.next() //{value:undefined,done:true} 

我们可以通过生成器很简便地写 iterator 接口

class O {
  constructor(p = []) {
    p.forEach(([key, value]) => (this[key] = value))
  }
  *[Symbol.iterator]() {
    const keys = Object.keys(this)
    for (let i = 0; i < keys.length; i++) {
      yield this[keys[i]]
    }
  }
}
const c = new O([
  ['a', 1],
  ['b', 2],
  ['c', 3]
])
for (let value of c) {
  console.log(value)
}
// 1 2 3 

yield * [Iterator] 迭代器委托

将迭代委托给另一个迭代器

let delegatedIterator = (function* () {
  yield 'Hello!';
  yield 'Bye!';
}());

let delegatingIterator = (function* () {
  yield 'Greetings!';
  yield* delegatedIterator;
  yield 'Ok, bye.';
}());

for(let value of delegatingIterator) {
  console.log(value);
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye." 

关于抛出错误,以及原型上的方法请看 Generator 函数的语法

generator函数的原理

由 switch case 组成的状态机模型中, 除此之外,利用闭包技巧,保存生成器函数上下文信息。

Regenerator 通过工具函数将生成器函数包装,为其添加如 next/return 等方法。同时也对返回的生成器对象进行包装,使得对 next 等方法的调用,最终进入由 switch case 组成的状态机模型中。除此之外,利用闭包技巧,保存生成器函数上下文信息。

【转向 Javascript 系列】深入理解 Generators

简单实现

Async / Await / Generator 实现原理

// 生成器函数根据yield语句将代码分割为switch-case块,后续通过切换_context.prev和_context.next来分别执行各个case
function gen$(_context) {
  while (1) {
    switch (_context.prev = _context.next) {
      case 0:
        _context.next = 2;
        return 'result1';

      case 2:
        _context.next = 4;
        return 'result2';

      case 4:
        _context.next = 6;
        return 'result3';

      case 6:
      case "end":
        return _context.stop();
    }
  }
}

// 低配版context 
var context = {
  next:0,
  prev: 0,
  done: false,
  stop: function stop () {
    this.done = true
  }
}

// 低配版invoke
let gen = function() {
  return {
    next: function() {
      value = context.done ? undefined: gen$(context)
      done = context.done
      return {
        value,
        done
      }
    }
  }
} 

// 测试使用
var g = gen() 
g.next()  // {value: "result1", done: false}
g.next()  // {value: "result2", done: false}
g.next()  // {value: "result3", done: false}
g.next()  // {value: undefined, done: true} 

async函数 与 generator 函数

Async / Await / Generator 实现原理

我们知道,async 函数是 generator 函数的语法糖,它们有三点不同

  • async/await自带执行器,不需要手动调用next()就能自动执行下一步
  • async函数返回值是Promise对象,而Generator返回的是生成器对象
  • await能够返回Promise的resolve/reject的值
function run(gen) {
  //把返回值包装成promise
  return new Promise((resolve, reject) => {
    var g = gen()

    function _next(val) {
      //错误处理
      try {
        var res = g.next(val) 
      } catch(err) {
        return reject(err); 
      }
      if(res.done) {
        // 将最后 return 值返回
        return resolve(res.value);
      }
      //res.value包装为promise,以兼容yield后面跟基本类型的情况
      Promise.resolve(res.value).then(
        val => {
          // 递归执行_next 达到了自动执行next功能
          _next(val);
        }, 
        err => {
          //抛出错误
          g.throw(err)
        });
    }
    _next();
  });
}

function* myGenerator() {
  try {
    console.log(yield Promise.resolve(1)) 
    console.log(yield 2)   //2
    console.log(yield Promise.reject('error'))
  } catch (error) {
    console.log(error)
  }
}

const result = run(myGenerator)     //result是一个Promise
//输出 1 2 error 

原型链

如何判断数组类型?

  • xxx instanceof Array

  • xxx.construtor === Array

  • Array.isArray(xxx) === true

  • Object.prototype.toString.call(xxx) === 'object Array'

描述new的过程

​ 1. 创建一个新对象

​ 2. this 指向这个新对象

​ 3. 执行代码,即对 this 赋值

​ 4. 返回 this

instanceof的原理

instance instanceof constructor

  • 判断实例对象 instance__proto__ 与构造函数 constuctorprototype 是不是引用的同一个原型对象

  • 若不是,沿instance的原型链继续向上找

    function myInstanceof(l, r) {
            while (l) {
                if (l.__proto__ == r.prototype) {
                    return true
                }
                l = l.__proto__
            }
            return false
    } 

对象的遍历方法

  • Object.prototype.entries:返回对象自身自身的所有可枚举的属性名和值对的数组。

  • Object.keys():返回对象自身的所有可枚举的属性的键名。

  • JSON.stringify():只串行化对象自身的可枚举的属性。

  • Object.assign(): 忽略enumerablefalse的属性,只拷贝对象自身的可枚举的属性。

特别注意

for in 会遍历到原型上的属性,需要配合 hasOwnProperty

for (let key in obj) {        
    if (obj.hasOwnProperty(key)){
        // dosomething
    }
} 

继承

ES5 中几种继承方法

JS原型链与继承别再被问倒了

  • 原型链继承

    将子类的原型赋值为父类实例,缺点是子类不能改变传入父类构造函数的参数,且当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;

    Child.prototype = new Parent('parent'); 
  • 构造函数内部继承

    在子类构造函数内call父类函数并传入this和参数,缺点是只能继承父类构造函数内赋予的属性

    function Child(){
        Parent.call(this,'yellow'); // 这句代码就是借助构造函数实现部分继承,绑定this并执行父构造函数
        this.type = 'child';
    } 
  • 组合继承(结合原型链继承和构造函数继承)

  • 原型式继承

    在object()函数内部, 先创建一个临时性的构造函数, 然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例.

    function object(o){
        function F(){}
        F.prototype = o;
        return new F();
    }
    var person = {
        friends : ["Van","Louis","Nick"]
    };
    var anotherPerson = object(person); 
  • 寄生式继承

    function createAnother(original){
        var clone = object(original);//通过调用object函数创建一个新对象
        clone.sayHi = function(){//以某种方式来增强这个对象(增强:为其添加属性或方法)
            alert("hi");
        };
        return clone;//返回这个对象
    } 
  • 组合寄生式

    • 第一步,将子类的原型赋值为一个空对象,这个对象的原型是父类,同时修改 constructor 属性,这一步的作用是将子类拽到父类的原型链上
    • 第二步,同理构造函数内部继承:在子类构造函数内call父类函数并传入this和参数
    function extend(subClass,superClass){
        var prototype = object(superClass.prototype);//创建对象,这个对象的原型是父类的原型
        prototype.constructor = subClass;//增强对象
        subClass.prototype = prototype;//指定对象
    }
    function Father(name){
        this.name = name;
    }
    Father.prototype.sayName = function(){
        alert(this.name);
    };
    function Son(name,age){
        Father.call(this,name);//继承实例属性,第一次调用Father()
        this.age = age;
    }
    //Son.prototype = new Father();
    //不创建新的父类实例而是用extend完成
    extend(Son,Father); 

ES6 class

继承

子类实例继承父类实例的属性和方法

  • 子类继承时,在构造函数内必须调用super方法,执行了父类的构造函数(与 ES5 中构造函数继承同理)

    class Point {
      constructor(x, y) {
        this.x = x;
        this.y = y;
      }
    }
    
    class ColorPoint extends Point {
      constructor(x, y, color) {
        super(x, y); // 不调用就报错
        this.color = color;
      }
    } 
  • 原型链继承

  • 构造函数作为对象, 构造函数的属性, 即静态方法继承

    // 上面两种都是 extends 后 js 自动完成
    class A {}
    class B extends {}
    
    A.prototype // {} 无属性
    A.prototype.__proto__ === B.prototype // true 相当于完成了组合寄生式的extends方法
    
    B.__proto__ === A // true 这样B可以访问到A上静态方法 

作用域

JavaScript是如何支持块级作用域的

变量提升

  • 变量声明: 把声明和赋值拆解 , 声明提升到作用域最前面 , 赋值保留在原位
  • 函数声明: 把函数声明 如同剪切一般, 整个提升到作用域前面(在变量声明后面).
  • if中变量声明,声明会提升;函数声明转换为变量声明,声明会提升。

let 和 var 的区别?

  • var 是函数作用域 ,let 是块级作用域

  • var有变量提升,let无变量提升

    准确得说,let和var的创建是都会提升,但是let在原声明前是禁止访问的,这也是造成暂时性死区的原因

  • let 不能在同一个作用域声明相同的变量 而 var没有此限制

  • let 在全局环境声明不会挂载到window上 而 var会

let的暂时性死区

暂时性死区:指块级作用域内,某个let声明的变量,在声明前的区域,无法访问作用域链上同名的变量。

JS中一个声明且赋值语句,它有三种状态:

​ 1.创建

创建在函数开始执行前就会完成,此时它还无法被访问,但是它会拦截函数上下文的访问并报错

let 在函数执行前只完成了创建

​ 2. 初始化

初始化后,变量可以被访问,此时它的值为undefined;var 变量在函数执行前就完成创建和初始化了,

let 的初始化在原句处

​ 3. 赋值

varlet 的赋值都在原句处

闭包是什么?

简单来说,所有引用了自由变量且不被销毁的函数就是闭包

自由变量就是既不是函数内参数又不是已声明的变量

你在实践中怎么运用闭包?

  • 解决for循环中setTimeout打印index最终数值一致的问题
  • 回调函数使用闭包改变外部的变量
  • 储存私有变量
    • 节流、防抖函数
    • React的高阶组件

深入闭包

作用域链和闭包:代码中出现相同的变量,JavaScript引擎如何选择

自由变量原本归属调用栈中 本层或本层以下 的调用上下文,但它不会随调用上下文而销毁

编译器创建一个闭包的步骤如下:

​ 1. 在每个函数执行前,它会编译并创建一个执行上下文

​ 2. 如果发现内部有函数定义,会快速扫描这个函数,如果函数使用了自由变量,则判定这个函数是一个闭包,并创建自由变量所属的执行上下文的闭包对象,这是个内部对象,存储在堆空间。

​ 3. 将自由变量挂载到闭包对象,如果后面有使用这个执行上下文的其他自由变量,也同样被挂载到这个执行上下文的闭包对象上;一个执行上下文对应一个闭包对象

​ 4. 没有被内部的函数使用的变量依旧在栈上

为什么闭包会导致内存泄漏?

不被销毁的闭包,与它相关的闭包对象都不会被销毁。

如果闭包不被执行,那么这个对象会一直占用内存。

同是使用变量,为何闭包就是内存泄漏?

函数中创建变量也是使用变量,闭包使用闭包变量上的变量也是使用变量,两者有着同样用途且都占用内存,为何说后者是内存泄漏?

首先,函数执行时创建的变量是执行时才创建,随调用上下文销毁而销毁;而与闭包相关的闭包对象与闭包共生,无论闭包是否执行都占用着一块内存。

第二,闭包在执行时,闭包变量会被使用,此时不是内存泄漏;闭包不被执行时,闭包变量无法被外界访问且一直占用内存,那么就是内存泄漏。

this

this的指向?

  • 全局环境指向window
  • 全局调用函数指向window
  • 对象调用函数指向对象
  • 箭头函数指向外部的this

以一个变量形式调用时,this 指向window,而不是调用这个函数的上下文的 this

关于 reference 如何影响 this,请看 JavaScript深入之从ECMAScript规范解读this

apply、call和bind

这三者的作用?

  • apply和call用于执行一个函数并强制改变其this的指向,差别在于参数的写法
  • bind基于传入的参数生成强制绑定this指向的函数

事件循环

为什么要区分宏任务、微任务?

如果不将任务进行划分,按照队列方式执行,当大量任务执行时,某些任务的回调迟迟得不到执行(都在队尾),就会造成应用效果上的卡顿。所以设计者将任务分为宏任务和微任务,微任务可以穿插在宏任务中执行。

哪些属于宏任务、微任务

宏任务:

  • script执行

  • 事件回调

  • setTimeout/setInterval

  • requestAnimationFrame

微任务:

  • promise.then
  • MutationObserver (用于监视dom的改变)

描述一下事件循环过程

执行一个宏任务,然后执行该宏任务中产生的微任务,如果微任务中产生了微任务,那么这个微任务也会被执行,直到微任务队列被清空,之后开启下一轮循环

关于事件循环,更详细请看

关于Promise和async的考题

async function foo() {
    console.log('foo')
}
async function bar() {
    console.log('bar start')
    await foo()
    console.log('bar end')
}
console.log('script start')
setTimeout(function () {
    console.log('setTimeout')
}, 0)
bar();
new Promise(function (resolve) {
    console.log('promise executor')
    resolve();
}).then(function () {
    console.log('promise then')
})
console.log('script end') 

​ 1. 首先在主协程中初始化异步函数foo和bar,碰到console.log打印script start;

​ 2. 解析到setTimeout,初始化一个Timer,创建一个新的task

​ 3. 执行bar函数,将控制权交给协程,输出bar start,碰到await,执行foo,输出foo,创建一个 Promise返回给主协程

​ 4. 将返回的promise添加到微任务队列,向下执行 new Promise,输出 promise executor,返回resolve 添加到微任务队列

​ 5. 输出script end

​ 6. 当前task结束之前检查微任务队列,执行第一个微任务,将控制器交给协程输出bar end

​ 7. 执行第二个微任务 输出 promise then

​ 8. 当前任务执行完毕进入下一个任务,输出setTimeout

特别注意 await 非 Promise 的值时,它会隐式创建 Promise 实例并 resolve 这个值

Promise的api

Promise[MDN]

Promise.all(iterable)

这个方法返回一个新的promise对象,该promise对象在iterable参数对象里所有的promise对象都成功的时候才会触发成功,一旦有任何一个iterable里面的promise对象失败则立即触发该promise对象的失败。这个新的promise对象在触发成功状态以后,会把一个包含iterable里所有promise返回值的数组作为成功回调的返回值,顺序跟iterable的顺序保持一致;如果这个新的promise对象触发了失败状态,它会把iterable里第一个触发失败的promise对象的错误信息作为它的失败错误信息。Promise.all方法常被用于处理多个promise对象的状态集合。

Promise.race(iterable)

首先 race 的返回值是一个 promise;当 iterable 参数里的任意一个 promise 成功或失败后,race 将这个 promise 的成功返回值或失败详情作为参数传入 race 返回的 promise 的 resolve 或 reject 中。

所以数组内的Promise实例,谁执行的快,就继承谁的执行结果和执行状态,不管是成功还是失败

//race方法 
Promise.race = function(promises){
  return new Promise((resolve,reject)=>{
    for(let i=0;i<promises.length;i++){
      promises[i].then(resolve,reject)
    };
  })
}
//all方法(获取所有的promise,都执行then,把结果放到数组,一起返回)
Promise.all = function(promises){
  let arr = [];
  let i = 0;
  function processData(index,data){
    arr[index] = data;
    i++;
    if(i == promises.length){
      resolve(arr);
    };
  };
  return new Promise((resolve,reject)=>{
    for(let i=0;i<promises.length;i++){
      promises[i].then(data=>{
        processData(i,data);
      },reject);
    };
  });
} 

Promise 符合规范的实现

史上最最最详细的手写Promise教程

// 来源 
class Promise{
  constructor(executor){
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks = [];
    let resolve = value => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        this.onResolvedCallbacks.forEach(fn=>fn());
      }
    };
    let reject = reason => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn=>fn());
      }
    };
    try{
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }
  then(onFulfilled,onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
    let promise2 = new Promise((resolve, reject) => {
      if (this.state === 'fulfilled') {
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      };
      if (this.state === 'rejected') {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      };
      if (this.state === 'pending') {
        this.onResolvedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          }, 0);
        });
        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onRejected(this.reason);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          }, 0)
        });
      };
    });
    return promise2;
  }
  catch(fn){
    return this.then(null,fn);
  }
}
function resolvePromise(promise2, x, resolve, reject){
  if(x === promise2){
    return reject(new TypeError('Chaining cycle detected for promise'));
  }
  let called;
  if (x != null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      let then = x.then;
      if (typeof then === 'function') { 
        then.call(x, y => {
          if(called)return;
          called = true;
          resolvePromise(promise2, y, resolve, reject);
        }, err => {
          if(called)return;
          called = true;
          reject(err);
        })
      } else {
        resolve(x);
      }
    } catch (e) {
      if(called)return;
      called = true;
      reject(e); 
    }
  } else {
    resolve(x);
  }
} 

拓展:vue异步批量更新

Vue源码详解之nextTick:MutationObserver只是浮云,microtask才是核心!

数据被修改触发setter函数,修改被watcher收集了,watcher将自己放入待更新的数组,在此次宏任务中的调用了this.$nextTick中的回调函数也被收集入回调的数组中;宏任务结束后,nextTick回调数组在微任务执行,nextTick回调数组中的第一个执行的函数就是Watcher数组先去通知更新vm实例更新,之后就按顺序执行被收集的nextTick回调。

调用微任务形式是:Promise.resolve 或 MutationObersever

手写代码

手写EmitEvent

class EventEmitter {
  constructor() {
    this.events = {}
  }
  on(eventName, callback = () => {}, once = false) {
    // const name = !!once ? 'onceEvents' : 'events'
    if (typeof callback !== 'function')
      throw new Error('callback must be a function')
    if (typeof eventName !== 'string')
      throw new Error('eventName must be a string')
    if (this.events[eventName] === undefined) this.events[eventName] = []
    this.events[eventName].push({
      callback,
      once
    })
  }
  once(eventName, callback) {
    this.on(eventName, callback, true)
  }
  off(eventName, callback) {
    if (typeof eventName !== 'string')
      throw new Error('eventName must be a string')
    const nameOfevent = this.events[eventName]
    if (Array.isArray(nameOfevent)) {
      this.events[eventName] =
        typeof callback !== 'function'
          ? []
          : nameOfevent.filter(e => e.callback !== callback)
    }
  }
  emit(eventName, ...args) {
    if (typeof eventName !== 'string')
      throw new Error('eventName must be a string')
    const nameOfevent = this.events[eventName]
    if (Array.isArray(nameOfevent)) {
      for (const e of nameOfevent) {
        e.callback.apply(this, args)
      }
      this.events[eventName] = nameOfevent.filter(e => !e.once)
    }
  }
} 

手写节流 throttle

节流函数的原理:

  • 闭包存储私有变量lock
  • 函数运行后上锁,并设置定时器解锁

复杂版, JavaScript专题之跟着 underscore 学节流

//节流函数

// 简洁版
function throttle(fn, { interval = 500 } = {}) {
  let lock = false
  return function (...args) {
    if (lock) return false
    lock = true
    setTimeout(() => {
      lock = false
    }, interval)
    return fn.apply(this, args)
  }
}

export function throttle(fn, {
    interval = 500
} = {}) {
    if (typeof fn != "function") return new Error("类型错误");
    const _self = fn;
    let timer,
        firstTime = true; // 是否第一次调用
    return function(...args) {
        const _me = this;
        if (firstTime) {
            fn.apply(_me, args);
            return (firstTime = false);
        }
        if (timer) {
            return false;
        }
        timer = setTimeout(() => {
            clearTimeout(timer);
            timer = null;
            _self.apply(_me, args);
        }, interval);
    };
} 

手写防抖 debounce

防抖函数计时器版原理:

  • 用闭包保存计时器引用 timer
  • 调用时清除计时器
  • 生成新的计时器

记录前一次运行的时间

复杂版,JavaScript专题之跟着underscore学防抖

// 开始执行方案 只执行第一次
function debounce(fn, { immediate = 500 } = {}) {
  let timestamp = 0
  return function (...args) {
    const pre = timestamp
    timestamp = Date.now()
    // if (!pre) return
    if (timestamp - pre >= immediate) return fn.apply(this, args)
  }
}

// 延迟执行方案 只执行最后一次 且最后一次也延迟
function debounce(fn, delay) {
  let timer = null
  return function (...args) {
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

// [JavaScript专题之跟着underscore学防抖] https://github.com/mqyqingfeng/Blog/issues/22
function debounce(func, wait, immediate) {

    var timeout;

    return function () {
        var context = this;
        var args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            // 如果已经执行过,不再执行
            var callNow = !timeout;
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            if (callNow) func.apply(context, args)
        }
        else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
    }
} 

手写apply和bind

先实现apply再实现bind

apply

function myApply(ctx,args = []){
    if(typeof this !== 'function') {
        throw new TypeError('not a function!')
    }
    const symbol = Symbol(0)
    ctx[symbol] = this
    const result = ctx[symbol](...args)
    delete ctx[symbol]
    return result
}
Fuction.prototype.apply = myApply 

bind

function myBind(ctx,...preArgs){
    const fn = this
    return (...args)=> fn.apply(ctx,[...preArgs,...args])
}
Function.prototype.bind = myBind 

手写curry化

curry化的作用:固定函数参数,减少参数的输入,参数的私有化;提高函数参数适用性,减少通用性;

  • fn.length可以得到原函数的参数个数
  • 通过已接收的参数个数判断继续curry还是执行
  • 注意参数的连接
function curry(fn, ...args) {
  // 继续接受参数然后柯里化
  return args.length < fn.length ? (...params) => {
    return curry(fn, ...args, ...params)
  } : fn(...args)
} 

手写深拷贝

  • 数组和对象类型区分创造然后递归下去
  • 原始类型直接返回
// 简单版
function deepCopy(obj) {
    if (typeof obj == "object") {
        const result = obj.constructor === Array ? [] : {}
        for (let key in obj) {
            if (obj.hasOwnProperty(key)) result[key] = deepCopy(obj[key])
        }
        return result
    } else return obj;
}

// 循环引用如何解决 ?
function deepCopy(obj) {
  // 使用 map 标记对象避免无限循环
  const map = new Map()

  function traverse(obj) {
    if (typeof obj == 'object' && !map.get(obj)) {
      map.set(obj, true)
      const result = obj.constructor === Array ? [] : {}
      for (let key in obj) {
        if (obj.hasOwnProperty(key)) result[key] = traverse(obj[key])
      }
      return result
    } else return obj
  }
  return traverse(obj)
} 

手写virtual Dom 生成真实 Dom节点

思路与深拷贝一模一样,都是递归遍历

result[key] = deepCopy(obj[key]) 替换成了 el.appendChild(createElement(child))

// 假设虚拟dom的结构
// {
//   tag:'div',        // 元素标签
//   attrs:{           // 属性
//     class:'a',
//     id:'b'
//   },
//   text:'我是内容',  // 文本内容
//   children:[]       // 子元素
// }

function createElement(virtualDom) {
  const { tag, attrs, text, children } = virtualDom
  const el = document.createElement(tag)
  Object.keys(attrs).forEach(key => el.setAttribute(key, attrs[key]))
  if (text !== null || text !== undefined) el.innerText = text
  for (let child of children) {
    el.appendChild(createElement(child))
  }
  return el
} 

手写new

  • 内置this
  • 设置this原型
  • 执行函数
  • 返回值判断
// 用于触发微任务的触发器类 正常使用setTimeout即可
class MicTaskTrigger {
  constructor(callback = () => {}) {
    this.counter = 1
    this.node = document.createTextNode(String(this.counter))
    this.callback = callback
    this.observer = new MutationObserver(() => {
      this.callback()
    })
    this.observer.observe(this.node, {
      characterData: true
    })
  }
  changeCallback(callback) {
    this.callback = callback
  }
  trigger(callback = () => {}) {
    this.callback = callback
    this.counter = (this.counter + 1) % 2
    this.node.data = String(this.counter)
  }
}

const mic = new MicTaskTrigger() // mic 是用于触发微任务的触发器 正常使用setTimeout即可
class MyPromise {
  constructor(fn) {
    // 三个状态
    this.state = 'pending' // fulfilled rejected
    this.value = undefined
    this.reason = undefined
    this.ResolvedCallbacks = []
    this.RejectedCallbacks = []
    let resolve = value => {
      if (this.state === 'pending') {
        this.state = 'fulfilled'
        this.value = value
        mic.trigger(() =>
          this.ResolvedCallbacks.forEach(callback =>
            callback.call(this, this.value)
          )
        )
      }
    }
    let reject = value => {
      if (this.state === 'pending') {
        this.state = 'rejected'
        this.reason = value
        if (this.RejectedCallbacks.length)
          mic.trigger(() => {
            this.RejectedCallbacks.forEach(callback =>
              callback.call(this, this.reason)
            )
          })
        else throw this.reason
      }
    }
    // 自动执行函数
    try {
      fn.call(this, resolve, reject)
    } catch (e) {
      reject(e)
    }
  }
  // then
  then(onFulfilled, onRejected) {
    if (typeof onFulfilled === 'function') {
      // 如果状态已经确定了,调用then时直接执行回调
      if (this.state === 'fulfilled')
        return mic.trigger(() => onFulfilled.call(this))
      this.ResolvedCallbacks.push(onFulfilled)
    }
    if (typeof onRejected === 'function') {
      if (this.state === 'rejected')
        return mic.trigger(() => onRejected.call(this))
      this.RejectedCallbacks.push(onRejected)
    }
  }
  catch(onRejected) {
    this.then(null, onRejected)
  }
}

const promise = new MyPromise((resolve, reject) => {
  console.log(111)
  resolve({ data: 100 })
})
promise.then(res => console.log(++res.data))
promise.then(res => console.log(++res.data)) 

手写Promise

  • 完成了 异步执行回调,then 和 catch 单次调用传入回调,状态固定后调用 then 直接回调;

    没有链式调用,没有处理返回值是 Promise 的情况,没有错误冒泡 和 then 内的错误处理

  • 状态

    • state 当前状态
    • value 调用 resolve 传入的值,即成功的结果
    • reason 调用 reject 传入的值,即失败的结果
    • ResolvedCallbacks 收集成功的回调
    • RejectedCallbacks 收集失败的回调
  • 构造函数内

    • 定义 resolved 和 rejected,作用是在 pending 状态下,改变状态并将回调函数放入事件队列中

    • 在 try-catch 中执行用户传入的函数,并传入以上两个函数,交予用户改变状态权力

    • catch 中调用 reject

  • then 函数中收集传入的成功和失败回调;如果是状态已定,直接将传入的回调函数放到队列中,无需收集

  • catch 执行 this.then(null, onRejected)

// 用于触发微任务的触发器类 正常使用setTimeout即可
class MicTaskTrigger {
  constructor(callback = () => {}) {
    this.counter = 1
    this.node = document.createTextNode(String(this.counter))
    this.callback = callback
    this.observer = new MutationObserver(() => {
      this.callback()
    })
    this.observer.observe(this.node, {
      characterData: true
    })
  }
  changeCallback(callback) {
    this.callback = callback
  }
  trigger(callback = () => {}) {
    this.callback = callback
    this.counter = (this.counter + 1) % 2
    this.node.data = String(this.counter)
  }
}

const mic = new MicTaskTrigger() // mic 是用于触发微任务的触发器 正常使用setTimeout即可
class MyPromise {
  constructor(fn) {
    // 三个状态
    this.state = 'pending' // fulfilled rejected
    this.value = undefined
    this.reason = undefined
    this.ResolvedCallbacks = []
    this.RejectedCallbacks = []
    let resolve = value => {
      if (this.state === 'pending') {
        this.state = 'fulfilled'
        this.value = value
        mic.trigger(() =>
          this.ResolvedCallbacks.forEach(callback =>
            callback.call(this, this.value)
          )
        )
      }
    }
    let reject = value => {
      if (this.state === 'pending') {
        this.state = 'rejected'
        this.reason = value
        if (this.RejectedCallbacks.length)
          mic.trigger(() => {
            this.RejectedCallbacks.forEach(callback =>
              callback.call(this, this.reason)
            )
          })
        else throw this.reason
      }
    }
    // 自动执行函数
    try {
      fn.call(this, resolve, reject)
    } catch (e) {
      reject(e)
    }
  }
  // then
  then(onFulfilled, onRejected) {
    if (typeof onFulfilled === 'function') {
      // 如果状态已经确定了,调用then时直接执行回调
      if (this.state === 'fulfilled')
        return mic.trigger(() => onFulfilled.call(this, this.value))
      this.ResolvedCallbacks.push(onFulfilled)
    }
    if (typeof onRejected === 'function') {
      if (this.state === 'rejected')
        return mic.trigger(() => onRejected.call(this, this.reason))
      this.RejectedCallbacks.push(onRejected)
    }
  }
  catch(onRejected) {
    this.then(null, onRejected)
  }
}

const promise1 = new MyPromise((resolve, reject) => {
  console.log(111)
  setTimeout(() => resolve({ data: 100 }), 5000)
})
promise1.then(res => console.log(++res.data))
promise1.then(res => console.log(++res.data)) 

数组扁平化

let arr = [[1,2,2], [6,7,8, [11,12, [12,13,[14]]], 10]]
// 原生
arr = arr.flat(infinity)

// 递归法
function flatten(arr){
    let res = []
    arr.forEach(item => {
        // 判断item是否为数组
        if(Array.isArray(item)) res = res.concat(flatten(item))
        else res.push(item)
    })
    return res
} 

setTimeout 实现 setInterval

因为事件循环的机制,setInterval 可能会出现两次或多次任务执行间隔远小于设置的间隔时间的情况

比如,在设置 setInterval 执行后,执行一个密集计算的任务;第一个时间点,setInterval的一个回调推入宏任务队列,此时密集计算任务仍未完成;到第二个时间点, setInterval的第二个回调推入宏任务队列,此时宏任务队列中,两个任务是连着的,最终导致两个任务连续执行而远小于设置间隔的情况。

setTimeout实现setInterval原理是setTimeout的回调内递归调用,可以保证两个任务的执行间隔至少大于设置的间隔。

详细可以看 《JavaScript高级程序设计》22.3 高级定时器

// 简单实现
function mySetInterval(fn, millisec){
  function interval(){
    setTimeout(interval, millisec);
    fn();
  }
  setTimeout(interval, millisec)
}

// 加上执行次数和取消定时器,类写法
class MySetInterval {
  constructor(fn, { interval, count = Infinity } = {}) {
    this.fn = fn
    this.interval = interval
    this.count = count
    this._count = 0 // 使用计数
    this._isOn = false
    this._timer = null
  }
  _interval() {
    this._timer = setTimeout(() => this._interval(), this.interval)
    this.fn()
    if (++this._count === this.count) this.off()
  }
  on() {
    if (this._isOn) return
    this._isOn = true
    this._timer = setTimeout(() => this._interval(), this.interval)
  }
  off() {
    if (!this._isOn) return
    this._isOn = false
    this._count = 0
    clearTimeout(this._timer)
    this._timer = null
  }
}

const itt = new MySetInterval(()=>console.log(111),{interval:1000,count:5 })
itt.on() // 111 * 5
itt.on() // 111 * 3
itt.off() // 停止后续 

手写响应式

let obj = {}
let input = document.getElementById('input')
let span = document.getElementById('span')
// 数据劫持
Object.defineProperty(obj, 'text', {
  configurable: true,
  enumerable: true,
  get() {
    console.log('获取数据了')
  },
  set(newVal) {
    console.log('数据更新了')
    input.value = newVal
    span.innerHTML = newVal
  }
})
// 输入监听
input.addEventListener('keyup', function(e) {
  obj.text = e.target.value
}) 

实现 v-show

如何用原生 JS 实现一个最简单的 v-show 指令?

看到题目不要慌了,考察的还是上面的响应式

<button onClick="model.isShow = true">显示</button>
<button onClick="model.isShow = false">隐藏</button>

<div v-show="isShow">Hello World!</div>

<script> // 第 1 步: 定义数据和视图
var model = {
  isShow: false
}
var view = document.querySelector('div')

// 第 2 步: 定义视图刷新方法
var updateView = function(value) {
  view.style.display = value ? '' : 'none'
}

// 第 3 步: 设置初始视图表现
var directiveKey = view.getAttribute('v-show')
updateView(model[directiveKey])

// 第 4 步: 监听数据变化,然后刷新视图,达到数据驱动的目的
Object.defineProperty(model, 'isShow', {
  set: function(val) {
    updateView(val)
  }
}) </script> 

CSS与HTML

如何理解html语义化?

  • 增加代码可读性
  • 有利于搜索引擎爬虫分析
  • 在css加载失败的情况下也能呈现完整的页面结构

CSS选择器与权重

CSS选择器的权重详解

选择器 表达式或示例 说明 权重
ID选择器 #aaa 100
类选择器 .aaa 10
标签选择器 h1 元素的tagName 1
属性选择器 [title] 详见这里 10
相邻选择器 selecter + selecter 拆分为两个选择器再计算
兄长选择器 selecter ~ selecter 拆分为两个选择器再计算
亲子选择器 selecter > selecter 拆分为两个选择器再计算
后代选择器 selecter selecter 拆分为两个选择器再计算
通配符选择器 * 0
各种伪类选择器 如:link, :visited, :hover, :active, :target, :root, :not等 10
各种伪元素 如::first-letter,::first-line,::after,::before,::selection 1
  • 1,0,0,0 > 0,99,99,99。也就是说从左往右逐个等级比较,前一等级相等才往后比。
  • 无论是行间、内部和外部样式,都是按照这个规则来进行比较。而不是直观的行间>内部>外部样式;ID>class>元素。之所以有这样的错觉,是因为确实行间为第一等的权重,所以它的权重是最高的。而内部样式可能一般写在了外部样式引用了之后,所以覆盖掉了之前的。
  • 在权重相同的情况下,后面的样式会覆盖掉前面的样式。
  • 通配符、子选择器、相邻选择器等的。虽然权值为0000,但是也比继承的样式优先,0 权值比无权值优先。

盒子模型 border-box 和 content-box的区别?

  • offsetWidth = border + padding + width

  • 当设置 box-sizing:border-box 时,offserWidth = width = border + padding + content,content是剩余下来的空间

    IE盒子模型默认 border-box

magin叠加

两个垂直外边距相遇时,他们将合为一个外边距

  • 兄弟节点 margin-top 和 margin-bottom 会叠加

  • 父子节点 margin-top 叠加 或者 margin-bottom 叠加

  • 一个元素没有内容,内边距和边框,它的margin-top 和 margin-bottom 会叠加

    margin-top:20px
    margin-bottom:20px
    叠加后 20px 
  • 上一种情况下,叠加后的垂直边距与其他元素的边距相遇后也同样会发生叠加

    如:多个空内容的p标签发生叠加的情况

    <p><p>
    <p>1<p>
    <p><p>
    
    这几个段落最终效果是只显示 <p>1<p> 
    因为其他p标签无内容,叠加后就消失了 
  • 接第二种情况,父子节点垂直边距叠加完后,仍会与父节点的兄弟节点叠加

解决:使用 BFC 包裹兄弟节点中的一个可以消除叠加的情况

margin负值问题

  • margin-left、margin-top 为负,影响自身,自身陷入前面的元素
  • margin-right、margin-bottom 为负,影响后面的元素,后面的元素陷入自身

BFC

格式化上下文[MDN]

BFC是什么?它的特点是什么?

BFC(block format content)块级格式化上下文,盒模型布局的CSS渲染模式,指一个独立的渲染区域或者说是一个隔离的独立容器。

BFC生成了新的渲染层,它可以解决同级边距折叠的问题,因为他们根本不再一个层面上。

特点:

  • 属于同一个BFC的两个相邻容器的上下margin会重叠

  • 元素的margin-left与其包含块的border-left相接触

  • bfc区域不会与float元素重叠

  • 计算bfc高度时,float元素也会被计入其中

  • bfc区域内的子元素不会影响外部元素

BFC的产生条件?

  • float不为none
  • position是absolute或者fixed
  • overflow不为visible
  • display为flex,inline-block等

absolute和relative定位的依据

  • relative依据自身

  • absolute依据最近的已定位(postion:relative,absolute,fixed)的祖先元素

Flex

Flex 布局教程:语法篇

Flex 布局教程:实例篇

flex属性

  • flex-direction 主轴方向 row column row-reverse column-reverse
  • flex-wrap 换行
  • justify-content 主轴内容如何排布
    • flex-start 开始端对齐
    • flex-end 结束端对齐
    • center 中心端对齐
    • space-between 两端对齐,项目之间的间隔都相等
    • space-around 每个项目两侧的间隔相等。所以,项目之间的间隔比项目与边框的间隔大一倍
  • align-items 交叉轴如何对齐
    • flex-start:交叉轴的起点对齐。
    • flex-end:交叉轴的终点对齐。
    • center:交叉轴的中点对齐。
    • baseline: 项目的第一行文字的基线对齐。
    • stretch(默认值):如果项目未设置高度或设为auto,将占满整个容器的高度。
  • align-content 多行的元素如何对齐
  • self-align 单个item的交叉轴如何对齐 属性与 align-items一样

flex:1?

css弹性盒子-------桃园三兄弟之:flex-grow、flex-shrink、flex-basis详解

flex: 1 分配父盒子的主轴大小,它其实是三种属性的简写

  • flex-grow: 1;
  • flex-shrink: 1;
  • flex-basis: auto;

以下默认 flex-direaction : row,主轴上的大小为 宽度

flex-basis

语义是盒子基础的宽度,

确定一个子盒子的宽度,优先级级比 width 高,比如 flex-basis:200px;width:100px,优先 flex-basis 的 200px 生效

flex-grow

语义是盒子如何 增大

当父元素的宽度大于子元素宽度之和时,子元素如何分配父元素的剩余宽度,也就是 会比 basis(基础) 的大小grow up(增大)

flex:1 能均分父盒子就是 flex-grow 在起作用

公式:

剩余宽度 = 父级的宽度 - 各个子元素的 flex—basis之和

自身在flex - grow 的占比 = 自身的 flex-grow /各个子元素 flex-grow 之和

宽度 = flex-basis + 剩余宽度 * flex-grow占比

flex-shrink

语义是盒子如何 收缩

当父元素的宽度小于于子元素宽度之和时,子元素如何缩小超出父元素的多余宽度,也就是 会比 basis(基础) 的大小shrink(缩小)

多余宽度 = 各个子元素的 flex—basis之和 - 父级的宽度

flex-shrink 加权 占比 = 自身的 flex-shrink 的加权 / 各个子元素的 flex-shrink 的加权之和

权重就是 flex-basis,flex-shrink加权 = flex-shrink * flex-basis

公式:宽度 = flex-basis - 多余宽度 * flex-shrink加权占比

居中

(水平、垂直、水平垂直) 居中

  • position + margin(适用于有对应的宽高)

    .h{
        position:absolute;
        left:50%;
        margin-left:-25px; /* 盒子宽度的一半 */    
    }
    
    .v{
        position:absolute;
        top:50%;
        margin-top:-25px; /* 盒子高度宽度的一半 */    
    }
    .vh{
        position:absolute;
        top:50%;
        left:50%
        margin-top:-25px;
        margin-left:-25px;
    } 
  • position + tansform

    .h{
        position:absolute;
        left:50%;
        transform:translate(-50%,0);    
    }
    
    .v{
        position:absolute;
        top:50%;
        transform:translate(0,-50%);    
    }
    .vh{
        position:absolute;
        top:50%;
        left:50%
        transform:translate(-50%,-50%);
    } 
  • flex

    .f{
        /* 父盒子 */
        display:flex
    }
    
    .h{
        justify-content:center    
    }
    
    .v{
        align-items:center    
    }
    .vh{
        justify-content:center;
        align-items:center;
    } 

水平居中独有的两种

  • margin: 0 auto; 适用于宽度确定的子盒子

  • 转换为行内块元素

    .h{
      display:inline-block;
      text-align:center;
    } 

垂直居中独有的

  • line-height 设置为 height 大小

水平垂直居中独有的

.vh{
    /* 可以保证浏览器兼容性 */
    position:absolute;
    left:0;
    right:0;
    top:0;
    bottom:0;
    margin:auto;
} 

浮动

清除浮动

给后面元素加上

.clear{
    clear:all
} 

给父盒子加上

.clear:after{
    clear:all
} 

给父盒子加上overflow 触发bfc(计算bfc高度时,float元素也会被计入其中)

.box{
    overflow:hidden
} 

line-height如何继承?

  • 直接写大小如:18px或20px,直接继承

  • 直接写比例如:1或者1.5,直接继承

  • 写百分比时如200%,先换算成父元素line-height大小再继承此大小而不是继承百分比

    .f{
        font-size:20px;
        line-height:200%;
    }
    .son{
        font-size:16px;
    }
    /* 子元素的line-height是40px */ 

移动端

rem是什么?

px:绝对像素

em:根据父元素的font-size确定

rem:根据根元素html的font-size确定

如何实现响应式?

css响应式

/* 根据屏幕宽度在media query 中设置 html的font-size */
@media only screen and (max-width: 320px){
    html {
        font-size: 5px !important;
    }
}
@media only screen and (min-width: 320px){
    html {
        font-size: 5px !important;
    }
}
@media only screen and (min-width: 384px){
    html {
        font-size: 6px !important;
    }
}
@media only screen and (min-width: 480px){
    html {
        font-size: 7.5px !important;
    }
}
/* 后续代码rem为单位时,1rem = 5px */ 

js动态设置

// 提前执行,初始化 resize 事件不会执行
setRem()
// 原始配置
function setRem () {
  let doc = document.documentElement
  let width = doc.getBoundingClientRect().width
  let rem = width / 75
  doc.style.fontSize = rem + 'px'
}
// 监听窗口变化
addEventListener("resize", setRem) 

vh 和 vw理解

首先理解 屏幕视口高度 [ window.screen.height ] 和 网页视口高度 [ window.innerHeight ]

前者是整个手机屏幕的高度,后者是去除导航栏等高度之后用于显示网页内容的高度;

window.innerHeight = 100vh

xcss

rpx是如何计算的?

小程序编译后,rpx会做一次px换算。换算是以375个物理像素为基准,也就是在一个宽度为375物理像素的屏幕下,1rpx = 1px。

举个例子:iPhone6屏幕宽度为375px,共750个物理像素,那么1rpx = 375 / 750 px = 0.5px。

本文转自 https://juejin.cn/post/6942796562464505863,如有侵权,请联系删除。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Easter79 Easter79
3年前
swap空间的增减方法
(1)增大swap空间去激活swap交换区:swapoff v /dev/vg00/lvswap扩展交换lv:lvextend L 10G /dev/vg00/lvswap重新生成swap交换区:mkswap /dev/vg00/lvswap激活新生成的交换区:swapon v /dev/vg00/lvswap
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
Java获得今日零时零分零秒的时间(Date型)
publicDatezeroTime()throwsParseException{    DatetimenewDate();    SimpleDateFormatsimpnewSimpleDateFormat("yyyyMMdd00:00:00");    SimpleDateFormatsimp2newS
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
劳伦斯
劳伦斯
Lv1
男 · ADC
一道残阳铺水中,半江瑟瑟半江红。
文章
5
粉丝
2
获赞
3