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 ?
-
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 // 相等 -
转换成整数运算
1/** 2 * 精确加法 3 */ 4function add(num1, num2) { 5 const num1Digits = (num1.toString().split('.')[1] || '').length; 6 const num2Digits = (num2.toString().split('.')[1] || '').length; 7 const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits)); 8 return (num1 * baseNum + num2 * baseNum) / baseNum; 9} 10add(0.1,0.2); // 0.3
类型转化与规则
转到boolean
除了以下 5 种,其他都被转为 true
- undefined
- null
- 0(包括+0和-0)
- NaN
- "" 空字符串
toString
1toString(10,2) // "1010" 输入数字时,第二个参数可选为进制 2 3toString(new Date()) // Wed Jan 20 2021 20:06:24 GMT+0800 (中国标准时间) 4 5toString([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 类型先
valueOf后toString, - Date 类型先
toString后valueOf
考虑到这两种类型实际都是调用了 toString 而 valueOf 并未改变输出 (除包装类型外),
所以默认对象的隐式转换都是调用了 toString
1[] == false // true []先调用 toString转化为"",""取布尔是false 2![] // false 3{} + 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 先转为数字
1undefined + true // NaN 2 3null + true // 1
5.不断从上往下检索规则,直到两边都是字符串或者数字。
注意 +a和 b + a,对 a的转换是不一样的:+a 是转换到 number,而 b + a 需要按照上述规则进行转换后相加
a == b 的转换
对象(非包装)比较或者不同类型比较时:
1.两边是对象对比地址
2.一边是对象先调用 toString(实际是 toPrimitive)
3.一边是布尔值转换为数字
4.两边分别是数字和字符串时,字符串转换为数字
5.不断从上往下检索规则,直到两边类型相同。
其他情况
1null == undefined // true undefined 和 null 不与其他假值 `==` 2 3NaN == NaN // false 需要判断 NaN,应该用 Number.isNaN
实例
-
'true' == true // false
- 符合 3,true 转为 1
- 符合 4,"true" 转为 NaN
- NaN == 1 返回false
-
[] == ![] // true
-
![] 转化为 false:除了 0,"",NaN,undefined,null,其他转布尔时都转为true,再取反为false
-
符合 2 和 3,[] 转为 "",false 转为 0
-
符合 4 ,"" 转为 0
-
0 == 0 返回 true
-
语言内置
关于Symbol
关于Symbol,使用得不多,它的作用是作为一个唯一值,作对象的键,防止属性被覆盖或覆盖已存在属性
如手写 apply,为了防止覆盖传入的函数上的属性,我们可以用Symbol作为键
1function myApply(ctx,args = []){ 2 if(typeof this !== 'function') { 3 throw new TypeError('not a function!') 4 } 5 const symbol = Symbol(0) 6 ctx[symbol] = this 7 const result = ctx[symbol](...args) 8 delete ctx[symbol] 9 return result 10} 11Fuction.prototype.apply = myApply
它第二个作用,它提供我们访问内置方法和覆写内置行为的可能。
Symbol上储存着各种内置方法的键,
通过重写类上的迭代器,可以改变实例使用迭代器的行为如
1class Collection { 2 *[Symbol.iterator]() { 3 let i = 0; 4 while(this[i] !== undefined) { 5 yield this[i]; 6 ++i; 7 } 8 } 9} 10 11let myCollection = new Collection(); 12myCollection[0] = 1; 13myCollection[1] = 2; 14 15for(let value of myCollection) { 16 console.log(value); 17}
迭代器(iterator)
迭代器是一个对象,它符合以下规范
-
对象上可访问 next 函数
-
next 函数 返回 {value,done},value为本轮迭代的值,done为布尔值,表示迭代是否结束
-
迭代器对象可以通过重复调用next()显式地迭代。 迭代一个迭代器被称为消耗了这个迭代器,因为它通常只能执行一次。 在产生终止值之后,对next()的额外调用应该继续返回{done:true}。
1var it = makeIterator(['a', 'b']); 2 3it.next() // { value: "a", done: false } 4it.next() // { value: "b", done: false } 最后一个值done为false,下一轮再next done为true 5it.next() // { value: undefined, done: true } 6 7function makeIterator(array) { 8 var nextIndex = 0; 9 return { 10 next: function() { 11 return nextIndex < array.length ? 12 {value: array[nextIndex++], done: false} : 13 {value: undefined, done: true}; 14 } 15 }; 16}
迭代器接口(iterator)
ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”。
它是一个返回迭代器的函数。
我们可以通过 Symbol.interator 访问
1let arr = ['a', 'b', 'c']; 2let iter = arr[Symbol.iterator](); 3 4iter.next() // { value: 'a', done: false } 5iter.next() // { value: 'b', done: false } 6iter.next() // { value: 'c', done: false } 7iter.next() // { value: undefined, done: true }
原生具备 Iterator 接口的数据结构如下。
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
生成器函数(generator)
生成器因为出现不久后就被 async 函数取代了,我学习时已经遍布 async 语法了。
不过我们还是很有必要了解它的基础语法,因为它与迭代器有关,进而与 for of,解构语法等有关;
同时 async 是 generator 函数的语法糖,了解了 gennerator 的原理后 async 的原理也就很好理解了。
生成器生成什么?生成迭代器
生成器函数(generator)function * name{} 是一个返回迭代器的函数,这个迭代器可以自动维护自己的状态。
基本用法
-
在
function关键词后添加*,声明生成器函数,调用生成器函数后,返回一个迭代器; -
yield是迭代器调用next后的执行暂停处,继续调用next执行到下一个next; -
yield后表达式的结果作为next的返回值; -
next传入的参数作为上一个next暂停处整个yield表达式的结果 -
生成器函数最后 return 没有
yield的效果,但是它会被保留在后续第一次调用next返回对象的value上
1function* f() { 2 for (let i=0; i<3; i++){ 3 if(yield i) yield 10 // 返回并记录函数状态 4 } 5 return 20 6} 7const iter = f() 8iter.next() //第三条 {value:0,done:false} 9iter.next() // {value:1,done:false} 10iter.next(true) //第四条 {value:10,done:false} 上一个 yield 是 if(yield i) 传入true,if成功,执行到 yield 10 11iter.next(true) // {value:2,done:false} 上一个 yield 是 yield 10 传入true 无影响 12iter.next() //第五条 {value:20,done:true} 返回值 20 被保存了 13iter.next() //{value:undefined,done:true}
我们可以通过生成器很简便地写 iterator 接口
1class O { 2 constructor(p = []) { 3 p.forEach(([key, value]) => (this[key] = value)) 4 } 5 *[Symbol.iterator]() { 6 const keys = Object.keys(this) 7 for (let i = 0; i < keys.length; i++) { 8 yield this[keys[i]] 9 } 10 } 11} 12const c = new O([ 13 ['a', 1], 14 ['b', 2], 15 ['c', 3] 16]) 17for (let value of c) { 18 console.log(value) 19} 20// 1 2 3
yield * [Iterator] 迭代器委托
将迭代委托给另一个迭代器
1let delegatedIterator = (function* () { 2 yield 'Hello!'; 3 yield 'Bye!'; 4}()); 5 6let delegatingIterator = (function* () { 7 yield 'Greetings!'; 8 yield* delegatedIterator; 9 yield 'Ok, bye.'; 10}()); 11 12for(let value of delegatingIterator) { 13 console.log(value); 14} 15// "Greetings! 16// "Hello!" 17// "Bye!" 18// "Ok, bye."
关于抛出错误,以及原型上的方法请看 Generator 函数的语法
generator函数的原理
由 switch case 组成的状态机模型中, 除此之外,利用闭包技巧,保存生成器函数上下文信息。
Regenerator 通过工具函数将生成器函数包装,为其添加如 next/return 等方法。同时也对返回的生成器对象进行包装,使得对 next 等方法的调用,最终进入由 switch case 组成的状态机模型中。除此之外,利用闭包技巧,保存生成器函数上下文信息。
【转向 Javascript 系列】深入理解 Generators
简单实现
Async / Await / Generator 实现原理
1// 生成器函数根据yield语句将代码分割为switch-case块,后续通过切换_context.prev和_context.next来分别执行各个case 2function gen$(_context) { 3 while (1) { 4 switch (_context.prev = _context.next) { 5 case 0: 6 _context.next = 2; 7 return 'result1'; 8 9 case 2: 10 _context.next = 4; 11 return 'result2'; 12 13 case 4: 14 _context.next = 6; 15 return 'result3'; 16 17 case 6: 18 case "end": 19 return _context.stop(); 20 } 21 } 22} 23 24// 低配版context 25var context = { 26 next:0, 27 prev: 0, 28 done: false, 29 stop: function stop () { 30 this.done = true 31 } 32} 33 34// 低配版invoke 35let gen = function() { 36 return { 37 next: function() { 38 value = context.done ? undefined: gen$(context) 39 done = context.done 40 return { 41 value, 42 done 43 } 44 } 45 } 46} 47 48// 测试使用 49var g = gen() 50g.next() // {value: "result1", done: false} 51g.next() // {value: "result2", done: false} 52g.next() // {value: "result3", done: false} 53g.next() // {value: undefined, done: true}
async函数 与 generator 函数
Async / Await / Generator 实现原理
我们知道,async 函数是 generator 函数的语法糖,它们有三点不同
- async/await自带执行器,不需要手动调用next()就能自动执行下一步
- async函数返回值是Promise对象,而Generator返回的是生成器对象
- await能够返回Promise的resolve/reject的值
1function run(gen) { 2 //把返回值包装成promise 3 return new Promise((resolve, reject) => { 4 var g = gen() 5 6 function _next(val) { 7 //错误处理 8 try { 9 var res = g.next(val) 10 } catch(err) { 11 return reject(err); 12 } 13 if(res.done) { 14 // 将最后 return 值返回 15 return resolve(res.value); 16 } 17 //res.value包装为promise,以兼容yield后面跟基本类型的情况 18 Promise.resolve(res.value).then( 19 val => { 20 // 递归执行_next 达到了自动执行next功能 21 _next(val); 22 }, 23 err => { 24 //抛出错误 25 g.throw(err) 26 }); 27 } 28 _next(); 29 }); 30} 31 32function* myGenerator() { 33 try { 34 console.log(yield Promise.resolve(1)) 35 console.log(yield 2) //2 36 console.log(yield Promise.reject('error')) 37 } catch (error) { 38 console.log(error) 39 } 40} 41 42const result = run(myGenerator) //result是一个Promise 43//输出 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__与构造函数constuctor的prototype是不是引用的同一个原型对象 -
若不是,沿
instance的原型链继续向上找1function myInstanceof(l, r) { 2 while (l) { 3 if (l.__proto__ == r.prototype) { 4 return true 5 } 6 l = l.__proto__ 7 } 8 return false 9}
对象的遍历方法
-
Object.prototype.entries:返回对象自身自身的所有可枚举的属性名和值对的数组。 -
Object.keys():返回对象自身的所有可枚举的属性的键名。 -
JSON.stringify():只串行化对象自身的可枚举的属性。 -
Object.assign(): 忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。
特别注意
for in 会遍历到原型上的属性,需要配合 hasOwnProperty
1for (let key in obj) { 2 if (obj.hasOwnProperty(key)){ 3 // dosomething 4 } 5}
继承
ES5 中几种继承方法
-
原型链继承
将子类的原型赋值为父类实例,缺点是子类不能改变传入父类构造函数的参数,且当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;
Child.prototype = new Parent('parent'); -
构造函数内部继承
在子类构造函数内call父类函数并传入this和参数,缺点是只能继承父类构造函数内赋予的属性
1function Child(){ 2 Parent.call(this,'yellow'); // 这句代码就是借助构造函数实现部分继承,绑定this并执行父构造函数 3 this.type = 'child'; 4} -
组合继承(结合原型链继承和构造函数继承)
-
原型式继承
在object()函数内部, 先创建一个临时性的构造函数, 然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例.
1function object(o){ 2 function F(){} 3 F.prototype = o; 4 return new F(); 5} 6var person = { 7 friends : ["Van","Louis","Nick"] 8}; 9var anotherPerson = object(person); -
寄生式继承
1function createAnother(original){ 2 var clone = object(original);//通过调用object函数创建一个新对象 3 clone.sayHi = function(){//以某种方式来增强这个对象(增强:为其添加属性或方法) 4 alert("hi"); 5 }; 6 return clone;//返回这个对象 7} -
组合寄生式
- 第一步,将子类的原型赋值为一个空对象,这个对象的原型是父类,同时修改 constructor 属性,这一步的作用是将子类拽到父类的原型链上
- 第二步,同理构造函数内部继承:在子类构造函数内call父类函数并传入this和参数
1function extend(subClass,superClass){ 2 var prototype = object(superClass.prototype);//创建对象,这个对象的原型是父类的原型 3 prototype.constructor = subClass;//增强对象 4 subClass.prototype = prototype;//指定对象 5} 6function Father(name){ 7 this.name = name; 8} 9Father.prototype.sayName = function(){ 10 alert(this.name); 11}; 12function Son(name,age){ 13 Father.call(this,name);//继承实例属性,第一次调用Father() 14 this.age = age; 15} 16//Son.prototype = new Father(); 17//不创建新的父类实例而是用extend完成 18extend(Son,Father);
ES6 class
继承
子类实例继承父类实例的属性和方法
-
子类继承时,在构造函数内必须调用super方法,执行了父类的构造函数(与 ES5 中构造函数继承同理)
1class Point { 2 constructor(x, y) { 3 this.x = x; 4 this.y = y; 5 } 6} 7 8class ColorPoint extends Point { 9 constructor(x, y, color) { 10 super(x, y); // 不调用就报错 11 this.color = color; 12 } 13} -
原型链继承
-
构造函数作为对象, 构造函数的属性, 即静态方法继承
1// 上面两种都是 extends 后 js 自动完成 2class A {} 3class B extends {} 4 5A.prototype // {} 无属性 6A.prototype.__proto__ === B.prototype // true 相当于完成了组合寄生式的extends方法 7 8B.__proto__ === A // true 这样B可以访问到A上静态方法
作用域
变量提升
- 变量声明: 把声明和赋值拆解 , 声明提升到作用域最前面 , 赋值保留在原位
- 函数声明: 把函数声明 如同剪切一般, 整个提升到作用域前面(在变量声明后面).
- 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. 赋值
var 和 let 的赋值都在原句处
闭包是什么?
简单来说,所有引用了自由变量且不被销毁的函数就是闭包
自由变量就是既不是函数内参数又不是已声明的变量
你在实践中怎么运用闭包?
- 解决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的考题
1async function foo() { 2 console.log('foo') 3} 4async function bar() { 5 console.log('bar start') 6 await foo() 7 console.log('bar end') 8} 9console.log('script start') 10setTimeout(function () { 11 console.log('setTimeout') 12}, 0) 13bar(); 14new Promise(function (resolve) { 15 console.log('promise executor') 16 resolve(); 17}).then(function () { 18 console.log('promise then') 19}) 20console.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.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实例,谁执行的快,就继承谁的执行结果和执行状态,不管是成功还是失败
1//race方法 2Promise.race = function(promises){ 3 return new Promise((resolve,reject)=>{ 4 for(let i=0;i<promises.length;i++){ 5 promises[i].then(resolve,reject) 6 }; 7 }) 8} 9//all方法(获取所有的promise,都执行then,把结果放到数组,一起返回) 10Promise.all = function(promises){ 11 let arr = []; 12 let i = 0; 13 function processData(index,data){ 14 arr[index] = data; 15 i++; 16 if(i == promises.length){ 17 resolve(arr); 18 }; 19 }; 20 return new Promise((resolve,reject)=>{ 21 for(let i=0;i<promises.length;i++){ 22 promises[i].then(data=>{ 23 processData(i,data); 24 },reject); 25 }; 26 }); 27}
Promise 符合规范的实现
1// 来源 2class Promise{ 3 constructor(executor){ 4 this.state = 'pending'; 5 this.value = undefined; 6 this.reason = undefined; 7 this.onResolvedCallbacks = []; 8 this.onRejectedCallbacks = []; 9 let resolve = value => { 10 if (this.state === 'pending') { 11 this.state = 'fulfilled'; 12 this.value = value; 13 this.onResolvedCallbacks.forEach(fn=>fn()); 14 } 15 }; 16 let reject = reason => { 17 if (this.state === 'pending') { 18 this.state = 'rejected'; 19 this.reason = reason; 20 this.onRejectedCallbacks.forEach(fn=>fn()); 21 } 22 }; 23 try{ 24 executor(resolve, reject); 25 } catch (err) { 26 reject(err); 27 } 28 } 29 then(onFulfilled,onRejected) { 30 onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value; 31 onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err }; 32 let promise2 = new Promise((resolve, reject) => { 33 if (this.state === 'fulfilled') { 34 setTimeout(() => { 35 try { 36 let x = onFulfilled(this.value); 37 resolvePromise(promise2, x, resolve, reject); 38 } catch (e) { 39 reject(e); 40 } 41 }, 0); 42 }; 43 if (this.state === 'rejected') { 44 setTimeout(() => { 45 try { 46 let x = onRejected(this.reason); 47 resolvePromise(promise2, x, resolve, reject); 48 } catch (e) { 49 reject(e); 50 } 51 }, 0); 52 }; 53 if (this.state === 'pending') { 54 this.onResolvedCallbacks.push(() => { 55 setTimeout(() => { 56 try { 57 let x = onFulfilled(this.value); 58 resolvePromise(promise2, x, resolve, reject); 59 } catch (e) { 60 reject(e); 61 } 62 }, 0); 63 }); 64 this.onRejectedCallbacks.push(() => { 65 setTimeout(() => { 66 try { 67 let x = onRejected(this.reason); 68 resolvePromise(promise2, x, resolve, reject); 69 } catch (e) { 70 reject(e); 71 } 72 }, 0) 73 }); 74 }; 75 }); 76 return promise2; 77 } 78 catch(fn){ 79 return this.then(null,fn); 80 } 81} 82function resolvePromise(promise2, x, resolve, reject){ 83 if(x === promise2){ 84 return reject(new TypeError('Chaining cycle detected for promise')); 85 } 86 let called; 87 if (x != null && (typeof x === 'object' || typeof x === 'function')) { 88 try { 89 let then = x.then; 90 if (typeof then === 'function') { 91 then.call(x, y => { 92 if(called)return; 93 called = true; 94 resolvePromise(promise2, y, resolve, reject); 95 }, err => { 96 if(called)return; 97 called = true; 98 reject(err); 99 }) 100 } else { 101 resolve(x); 102 } 103 } catch (e) { 104 if(called)return; 105 called = true; 106 reject(e); 107 } 108 } else { 109 resolve(x); 110 } 111}
拓展:vue异步批量更新
Vue源码详解之nextTick:MutationObserver只是浮云,microtask才是核心!
数据被修改触发setter函数,修改被watcher收集了,watcher将自己放入待更新的数组,在此次宏任务中的调用了this.$nextTick中的回调函数也被收集入回调的数组中;宏任务结束后,nextTick回调数组在微任务执行,nextTick回调数组中的第一个执行的函数就是Watcher数组先去通知更新vm实例更新,之后就按顺序执行被收集的nextTick回调。
调用微任务形式是:Promise.resolve 或 MutationObersever
手写代码
手写EmitEvent
1class EventEmitter { 2 constructor() { 3 this.events = {} 4 } 5 on(eventName, callback = () => {}, once = false) { 6 // const name = !!once ? 'onceEvents' : 'events' 7 if (typeof callback !== 'function') 8 throw new Error('callback must be a function') 9 if (typeof eventName !== 'string') 10 throw new Error('eventName must be a string') 11 if (this.events[eventName] === undefined) this.events[eventName] = [] 12 this.events[eventName].push({ 13 callback, 14 once 15 }) 16 } 17 once(eventName, callback) { 18 this.on(eventName, callback, true) 19 } 20 off(eventName, callback) { 21 if (typeof eventName !== 'string') 22 throw new Error('eventName must be a string') 23 const nameOfevent = this.events[eventName] 24 if (Array.isArray(nameOfevent)) { 25 this.events[eventName] = 26 typeof callback !== 'function' 27 ? [] 28 : nameOfevent.filter(e => e.callback !== callback) 29 } 30 } 31 emit(eventName, ...args) { 32 if (typeof eventName !== 'string') 33 throw new Error('eventName must be a string') 34 const nameOfevent = this.events[eventName] 35 if (Array.isArray(nameOfevent)) { 36 for (const e of nameOfevent) { 37 e.callback.apply(this, args) 38 } 39 this.events[eventName] = nameOfevent.filter(e => !e.once) 40 } 41 } 42}
手写节流 throttle
节流函数的原理:
- 闭包存储私有变量lock
- 函数运行后上锁,并设置定时器解锁
复杂版, JavaScript专题之跟着 underscore 学节流
1//节流函数 2 3// 简洁版 4function throttle(fn, { interval = 500 } = {}) { 5 let lock = false 6 return function (...args) { 7 if (lock) return false 8 lock = true 9 setTimeout(() => { 10 lock = false 11 }, interval) 12 return fn.apply(this, args) 13 } 14} 15 16export function throttle(fn, { 17 interval = 500 18} = {}) { 19 if (typeof fn != "function") return new Error("类型错误"); 20 const _self = fn; 21 let timer, 22 firstTime = true; // 是否第一次调用 23 return function(...args) { 24 const _me = this; 25 if (firstTime) { 26 fn.apply(_me, args); 27 return (firstTime = false); 28 } 29 if (timer) { 30 return false; 31 } 32 timer = setTimeout(() => { 33 clearTimeout(timer); 34 timer = null; 35 _self.apply(_me, args); 36 }, interval); 37 }; 38}
手写防抖 debounce
防抖函数计时器版原理:
- 用闭包保存计时器引用 timer
- 调用时清除计时器
- 生成新的计时器
记录前一次运行的时间
复杂版,JavaScript专题之跟着underscore学防抖
1// 开始执行方案 只执行第一次 2function debounce(fn, { immediate = 500 } = {}) { 3 let timestamp = 0 4 return function (...args) { 5 const pre = timestamp 6 timestamp = Date.now() 7 // if (!pre) return 8 if (timestamp - pre >= immediate) return fn.apply(this, args) 9 } 10} 11 12// 延迟执行方案 只执行最后一次 且最后一次也延迟 13function debounce(fn, delay) { 14 let timer = null 15 return function (...args) { 16 if (timer) { 17 clearTimeout(timer) 18 } 19 timer = setTimeout(() => fn.apply(this, args), delay) 20 } 21} 22 23// [JavaScript专题之跟着underscore学防抖] https://github.com/mqyqingfeng/Blog/issues/22 24function debounce(func, wait, immediate) { 25 26 var timeout; 27 28 return function () { 29 var context = this; 30 var args = arguments; 31 32 if (timeout) clearTimeout(timeout); 33 if (immediate) { 34 // 如果已经执行过,不再执行 35 var callNow = !timeout; 36 timeout = setTimeout(function(){ 37 timeout = null; 38 }, wait) 39 if (callNow) func.apply(context, args) 40 } 41 else { 42 timeout = setTimeout(function(){ 43 func.apply(context, args) 44 }, wait); 45 } 46 } 47}
手写apply和bind
先实现apply再实现bind
apply
1function myApply(ctx,args = []){ 2 if(typeof this !== 'function') { 3 throw new TypeError('not a function!') 4 } 5 const symbol = Symbol(0) 6 ctx[symbol] = this 7 const result = ctx[symbol](...args) 8 delete ctx[symbol] 9 return result 10} 11Fuction.prototype.apply = myApply
bind
1function myBind(ctx,...preArgs){ 2 const fn = this 3 return (...args)=> fn.apply(ctx,[...preArgs,...args]) 4} 5Function.prototype.bind = myBind
手写curry化
curry化的作用:固定函数参数,减少参数的输入,参数的私有化;提高函数参数适用性,减少通用性;
- fn.length可以得到原函数的参数个数
- 通过已接收的参数个数判断继续curry还是执行
- 注意参数的连接
1function curry(fn, ...args) { 2 // 继续接受参数然后柯里化 3 return args.length < fn.length ? (...params) => { 4 return curry(fn, ...args, ...params) 5 } : fn(...args) 6}
手写深拷贝
- 数组和对象类型区分创造然后递归下去
- 原始类型直接返回
1// 简单版 2function deepCopy(obj) { 3 if (typeof obj == "object") { 4 const result = obj.constructor === Array ? [] : {} 5 for (let key in obj) { 6 if (obj.hasOwnProperty(key)) result[key] = deepCopy(obj[key]) 7 } 8 return result 9 } else return obj; 10} 11 12// 循环引用如何解决 ? 13function deepCopy(obj) { 14 // 使用 map 标记对象避免无限循环 15 const map = new Map() 16 17 function traverse(obj) { 18 if (typeof obj == 'object' && !map.get(obj)) { 19 map.set(obj, true) 20 const result = obj.constructor === Array ? [] : {} 21 for (let key in obj) { 22 if (obj.hasOwnProperty(key)) result[key] = traverse(obj[key]) 23 } 24 return result 25 } else return obj 26 } 27 return traverse(obj) 28}
手写virtual Dom 生成真实 Dom节点
思路与深拷贝一模一样,都是递归遍历
result[key] = deepCopy(obj[key]) 替换成了 el.appendChild(createElement(child))
1// 假设虚拟dom的结构 2// { 3// tag:'div', // 元素标签 4// attrs:{ // 属性 5// class:'a', 6// id:'b' 7// }, 8// text:'我是内容', // 文本内容 9// children:[] // 子元素 10// } 11 12function createElement(virtualDom) { 13 const { tag, attrs, text, children } = virtualDom 14 const el = document.createElement(tag) 15 Object.keys(attrs).forEach(key => el.setAttribute(key, attrs[key])) 16 if (text !== null || text !== undefined) el.innerText = text 17 for (let child of children) { 18 el.appendChild(createElement(child)) 19 } 20 return el 21}
手写new
- 内置this
- 设置this原型
- 执行函数
- 返回值判断
1// 用于触发微任务的触发器类 正常使用setTimeout即可 2class MicTaskTrigger { 3 constructor(callback = () => {}) { 4 this.counter = 1 5 this.node = document.createTextNode(String(this.counter)) 6 this.callback = callback 7 this.observer = new MutationObserver(() => { 8 this.callback() 9 }) 10 this.observer.observe(this.node, { 11 characterData: true 12 }) 13 } 14 changeCallback(callback) { 15 this.callback = callback 16 } 17 trigger(callback = () => {}) { 18 this.callback = callback 19 this.counter = (this.counter + 1) % 2 20 this.node.data = String(this.counter) 21 } 22} 23 24const mic = new MicTaskTrigger() // mic 是用于触发微任务的触发器 正常使用setTimeout即可 25class MyPromise { 26 constructor(fn) { 27 // 三个状态 28 this.state = 'pending' // fulfilled rejected 29 this.value = undefined 30 this.reason = undefined 31 this.ResolvedCallbacks = [] 32 this.RejectedCallbacks = [] 33 let resolve = value => { 34 if (this.state === 'pending') { 35 this.state = 'fulfilled' 36 this.value = value 37 mic.trigger(() => 38 this.ResolvedCallbacks.forEach(callback => 39 callback.call(this, this.value) 40 ) 41 ) 42 } 43 } 44 let reject = value => { 45 if (this.state === 'pending') { 46 this.state = 'rejected' 47 this.reason = value 48 if (this.RejectedCallbacks.length) 49 mic.trigger(() => { 50 this.RejectedCallbacks.forEach(callback => 51 callback.call(this, this.reason) 52 ) 53 }) 54 else throw this.reason 55 } 56 } 57 // 自动执行函数 58 try { 59 fn.call(this, resolve, reject) 60 } catch (e) { 61 reject(e) 62 } 63 } 64 // then 65 then(onFulfilled, onRejected) { 66 if (typeof onFulfilled === 'function') { 67 // 如果状态已经确定了,调用then时直接执行回调 68 if (this.state === 'fulfilled') 69 return mic.trigger(() => onFulfilled.call(this)) 70 this.ResolvedCallbacks.push(onFulfilled) 71 } 72 if (typeof onRejected === 'function') { 73 if (this.state === 'rejected') 74 return mic.trigger(() => onRejected.call(this)) 75 this.RejectedCallbacks.push(onRejected) 76 } 77 } 78 catch(onRejected) { 79 this.then(null, onRejected) 80 } 81} 82 83const promise = new MyPromise((resolve, reject) => { 84 console.log(111) 85 resolve({ data: 100 }) 86}) 87promise.then(res => console.log(++res.data)) 88promise.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)
1// 用于触发微任务的触发器类 正常使用setTimeout即可 2class MicTaskTrigger { 3 constructor(callback = () => {}) { 4 this.counter = 1 5 this.node = document.createTextNode(String(this.counter)) 6 this.callback = callback 7 this.observer = new MutationObserver(() => { 8 this.callback() 9 }) 10 this.observer.observe(this.node, { 11 characterData: true 12 }) 13 } 14 changeCallback(callback) { 15 this.callback = callback 16 } 17 trigger(callback = () => {}) { 18 this.callback = callback 19 this.counter = (this.counter + 1) % 2 20 this.node.data = String(this.counter) 21 } 22} 23 24const mic = new MicTaskTrigger() // mic 是用于触发微任务的触发器 正常使用setTimeout即可 25class MyPromise { 26 constructor(fn) { 27 // 三个状态 28 this.state = 'pending' // fulfilled rejected 29 this.value = undefined 30 this.reason = undefined 31 this.ResolvedCallbacks = [] 32 this.RejectedCallbacks = [] 33 let resolve = value => { 34 if (this.state === 'pending') { 35 this.state = 'fulfilled' 36 this.value = value 37 mic.trigger(() => 38 this.ResolvedCallbacks.forEach(callback => 39 callback.call(this, this.value) 40 ) 41 ) 42 } 43 } 44 let reject = value => { 45 if (this.state === 'pending') { 46 this.state = 'rejected' 47 this.reason = value 48 if (this.RejectedCallbacks.length) 49 mic.trigger(() => { 50 this.RejectedCallbacks.forEach(callback => 51 callback.call(this, this.reason) 52 ) 53 }) 54 else throw this.reason 55 } 56 } 57 // 自动执行函数 58 try { 59 fn.call(this, resolve, reject) 60 } catch (e) { 61 reject(e) 62 } 63 } 64 // then 65 then(onFulfilled, onRejected) { 66 if (typeof onFulfilled === 'function') { 67 // 如果状态已经确定了,调用then时直接执行回调 68 if (this.state === 'fulfilled') 69 return mic.trigger(() => onFulfilled.call(this, this.value)) 70 this.ResolvedCallbacks.push(onFulfilled) 71 } 72 if (typeof onRejected === 'function') { 73 if (this.state === 'rejected') 74 return mic.trigger(() => onRejected.call(this, this.reason)) 75 this.RejectedCallbacks.push(onRejected) 76 } 77 } 78 catch(onRejected) { 79 this.then(null, onRejected) 80 } 81} 82 83const promise1 = new MyPromise((resolve, reject) => { 84 console.log(111) 85 setTimeout(() => resolve({ data: 100 }), 5000) 86}) 87promise1.then(res => console.log(++res.data)) 88promise1.then(res => console.log(++res.data))
数组扁平化
1let arr = [[1,2,2], [6,7,8, [11,12, [12,13,[14]]], 10]] 2// 原生 3arr = arr.flat(infinity) 4 5// 递归法 6function flatten(arr){ 7 let res = [] 8 arr.forEach(item => { 9 // 判断item是否为数组 10 if(Array.isArray(item)) res = res.concat(flatten(item)) 11 else res.push(item) 12 }) 13 return res 14}
setTimeout 实现 setInterval
因为事件循环的机制,setInterval 可能会出现两次或多次任务执行间隔远小于设置的间隔时间的情况
比如,在设置 setInterval 执行后,执行一个密集计算的任务;第一个时间点,setInterval的一个回调推入宏任务队列,此时密集计算任务仍未完成;到第二个时间点, setInterval的第二个回调推入宏任务队列,此时宏任务队列中,两个任务是连着的,最终导致两个任务连续执行而远小于设置间隔的情况。
setTimeout实现setInterval原理是setTimeout的回调内递归调用,可以保证两个任务的执行间隔至少大于设置的间隔。
详细可以看 《JavaScript高级程序设计》22.3 高级定时器
1// 简单实现 2function mySetInterval(fn, millisec){ 3 function interval(){ 4 setTimeout(interval, millisec); 5 fn(); 6 } 7 setTimeout(interval, millisec) 8} 9 10// 加上执行次数和取消定时器,类写法 11class MySetInterval { 12 constructor(fn, { interval, count = Infinity } = {}) { 13 this.fn = fn 14 this.interval = interval 15 this.count = count 16 this._count = 0 // 使用计数 17 this._isOn = false 18 this._timer = null 19 } 20 _interval() { 21 this._timer = setTimeout(() => this._interval(), this.interval) 22 this.fn() 23 if (++this._count === this.count) this.off() 24 } 25 on() { 26 if (this._isOn) return 27 this._isOn = true 28 this._timer = setTimeout(() => this._interval(), this.interval) 29 } 30 off() { 31 if (!this._isOn) return 32 this._isOn = false 33 this._count = 0 34 clearTimeout(this._timer) 35 this._timer = null 36 } 37} 38 39const itt = new MySetInterval(()=>console.log(111),{interval:1000,count:5 }) 40itt.on() // 111 * 5 41itt.on() // 111 * 3 42itt.off() // 停止后续
手写响应式
1let obj = {} 2let input = document.getElementById('input') 3let span = document.getElementById('span') 4// 数据劫持 5Object.defineProperty(obj, 'text', { 6 configurable: true, 7 enumerable: true, 8 get() { 9 console.log('获取数据了') 10 }, 11 set(newVal) { 12 console.log('数据更新了') 13 input.value = newVal 14 span.innerHTML = newVal 15 } 16}) 17// 输入监听 18input.addEventListener('keyup', function(e) { 19 obj.text = e.target.value 20})
实现 v-show
看到题目不要慌了,考察的还是上面的响应式
1<button onClick="model.isShow = true">显示</button> 2<button onClick="model.isShow = false">隐藏</button> 3 4<div v-show="isShow">Hello World!</div> 5 6<script> // 第 1 步: 定义数据和视图 7var model = { 8 isShow: false 9} 10var view = document.querySelector('div') 11 12// 第 2 步: 定义视图刷新方法 13var updateView = function(value) { 14 view.style.display = value ? '' : 'none' 15} 16 17// 第 3 步: 设置初始视图表现 18var directiveKey = view.getAttribute('v-show') 19updateView(model[directiveKey]) 20 21// 第 4 步: 监听数据变化,然后刷新视图,达到数据驱动的目的 22Object.defineProperty(model, 'isShow', { 23 set: function(val) { 24 updateView(val) 25 } 26}) </script>
CSS与HTML
如何理解html语义化?
- 增加代码可读性
- 有利于搜索引擎爬虫分析
- 在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 会叠加
1margin-top:20px 2margin-bottom:20px 3叠加后 20px -
上一种情况下,叠加后的垂直边距与其他元素的边距相遇后也同样会发生叠加
如:多个空内容的p标签发生叠加的情况
1<p><p> 2<p>1<p> 3<p><p> 4 5这几个段落最终效果是只显示 <p>1<p> 6因为其他p标签无内容,叠加后就消失了 -
接第二种情况,父子节点垂直边距叠加完后,仍会与父节点的兄弟节点叠加
解决:使用 BFC 包裹兄弟节点中的一个可以消除叠加的情况
margin负值问题
- margin-left、margin-top 为负,影响自身,自身陷入前面的元素
- margin-right、margin-bottom 为负,影响后面的元素,后面的元素陷入自身
BFC
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-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(适用于有对应的宽高)
1.h{ 2 position:absolute; 3 left:50%; 4 margin-left:-25px; /* 盒子宽度的一半 */ 5} 6 7.v{ 8 position:absolute; 9 top:50%; 10 margin-top:-25px; /* 盒子高度宽度的一半 */ 11} 12.vh{ 13 position:absolute; 14 top:50%; 15 left:50% 16 margin-top:-25px; 17 margin-left:-25px; 18} -
position + tansform
1.h{ 2 position:absolute; 3 left:50%; 4 transform:translate(-50%,0); 5} 6 7.v{ 8 position:absolute; 9 top:50%; 10 transform:translate(0,-50%); 11} 12.vh{ 13 position:absolute; 14 top:50%; 15 left:50% 16 transform:translate(-50%,-50%); 17} -
flex
1.f{ 2 /* 父盒子 */ 3 display:flex 4} 5 6.h{ 7 justify-content:center 8} 9 10.v{ 11 align-items:center 12} 13.vh{ 14 justify-content:center; 15 align-items:center; 16}
水平居中独有的两种
-
margin: 0 auto; 适用于宽度确定的子盒子
-
转换为行内块元素
1.h{ 2 display:inline-block; 3 text-align:center; 4}
垂直居中独有的
- line-height 设置为 height 大小
水平垂直居中独有的
1.vh{ 2 /* 可以保证浏览器兼容性 */ 3 position:absolute; 4 left:0; 5 right:0; 6 top:0; 7 bottom:0; 8 margin:auto; 9}
浮动
清除浮动
给后面元素加上
1.clear{ 2 clear:all 3}
给父盒子加上
1.clear:after{ 2 clear:all 3}
给父盒子加上overflow 触发bfc(计算bfc高度时,float元素也会被计入其中)
1.box{ 2 overflow:hidden 3}
line-height如何继承?
-
直接写大小如:18px或20px,直接继承
-
直接写比例如:1或者1.5,直接继承
-
写百分比时如200%,先换算成父元素line-height大小再继承此大小而不是继承百分比
1.f{ 2 font-size:20px; 3 line-height:200%; 4} 5.son{ 6 font-size:16px; 7} 8/* 子元素的line-height是40px */
移动端
rem是什么?
px:绝对像素
em:根据父元素的font-size确定
rem:根据根元素html的font-size确定
如何实现响应式?
css响应式
1/* 根据屏幕宽度在media query 中设置 html的font-size */ 2@media only screen and (max-width: 320px){ 3 html { 4 font-size: 5px !important; 5 } 6} 7@media only screen and (min-width: 320px){ 8 html { 9 font-size: 5px !important; 10 } 11} 12@media only screen and (min-width: 384px){ 13 html { 14 font-size: 6px !important; 15 } 16} 17@media only screen and (min-width: 480px){ 18 html { 19 font-size: 7.5px !important; 20 } 21} 22/* 后续代码rem为单位时,1rem = 5px */
js动态设置
1// 提前执行,初始化 resize 事件不会执行 2setRem() 3// 原始配置 4function setRem () { 5 let doc = document.documentElement 6 let width = doc.getBoundingClientRect().width 7 let rem = width / 75 8 doc.style.fontSize = rem + 'px' 9} 10// 监听窗口变化 11addEventListener("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,如有侵权,请联系删除。
