《整理3》

180 阅读12分钟

一. 数组扁平化

把多层嵌套的数组展成一维数组

1. arr.flat()

直接使用数组上的方法。flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。

var newArray = arr.flat([depth])

depth是可选的,默认是1.指定可以展开的嵌套数组的深度。

var arr1 = [1, 2, [3, 4]];
arr1.flat(); 
// [1, 2, 3, 4]

var arr2 = [1, 2, [3, 4, [5, 6]]];
arr2.flat();
// [1, 2, 3, 4, [5, 6]]

var arr3 = [1, 2, [3, 4, [5, 6]]];
arr3.flat(2);
// [1, 2, 3, 4, 5, 6]

//使用 Infinity,可展开任意深度的嵌套数组
var arr4 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]];
arr4.flat(Infinity);
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

2. reduce + 递归

var arr = [1, [2, [3, 4]]];

function flatten(arr) {
    return arr.reduce(function(result, item){
        return result.concat(Array.isArray(item) ? flatten(item) : item)
    }, [])
}

二. CSS3新特性

布局方面:新增了flex布局,

盒模型方面,新增了box-sizing设置盒模型

border-radius 设置圆角

transform 变换 translate rotate scale

动画方面新增了transitionanimation

三. 浏览器渲染机制,重排(回流)和重绘

浏览器首先下载html,css代码。下载完毕后:

  1. 浏览器解析html代码,创建一棵DOM 树,每个html标签在这个树上都有一个对应的节点,文本也有对应节点。

  2. 解析css代码,通过一堆hack使得它变得有意义,诸如-moz,-webkit和其它不能理解的拓展名,浏览器会大胆的把他们忽略。创建成CSSOM树

  3. 把两棵树合并成一棵渲染树(render tree),它是DOM树中可见的部分,比如一些不可见的head标签和被隐藏的节点,就不会出现在渲染树中。一旦渲染树创建成功,浏览器就可以在屏幕上绘制节点。

  4. 布局,计算元素的大小,位置,把所有节点画在屏幕上。

  5. 绘制,给元素添加边框颜色,背景颜色等等,显示出整个页面。

  6. 合成,根据层叠关系展示出页面。

最后的456就是渲染。渲染: 在页面的生命周期中,网页生成的时候,至少会渲染一次。在用户访问的过程中,还会不断触发重排(reflow)和重绘(repaint),不管页面发生了重绘还是重排,都会影响性能,最可怕的是重排,会使我们付出高额的性能代价,所以我们应尽量避免。

重排(回流)和重绘

重排:元素的尺寸或位置发生变化,导致要重新排列元素,重新生成布局的过程。

重绘:元素的外观,样式发生改变,如颜色,要重新把元素外观绘制出来的过程。

重排一定会导致重绘,重绘不一定导致重排。单单改变元素的外观,肯定不会引起网页重新生成布局,但当浏览器完成重排之后,将会重新绘制受到此次重排影响的部分。比如改变元素高度,这个元素乃至周边dom都需要重新绘制。

发生重排:添加/删除可见的DOM元素; 改变元素位置; 改变元素尺寸,比如边距、填充、边框、宽度和高度等; 改变元素内容,比如文字数量,图片大小等; 改变元素字体大小; 改变浏览器窗口尺寸,比如resize事件发生时; 激活CSS伪类(例如::hover); 设置 style 属性的值,因为通过设置style属性改变结点样式的话,每一次设置都会触发一次reflow; 查询某些属性或调用某些计算方法:offsetWidth、offsetHeight等,除此之外,当我们调用 getComputedStyle方法,或者IE里的 currentStyle 时,也会触发重排,原理是一样的,都为求一个“即时性”和“准确性”。

优化重排:

减少重排次数

  1. 样式集中改变: 不要频繁的操作样式,对于一个静态页面来说,更好的做法是更改类名而不是修改样式。对于动态改变的样式来说,相较每次微小修改都直接触及元素,更好的办法是统一在 cssText 变量中编辑。虽然现在大部分现代浏览器都会有 Flush 队列进行渲染队列优化,但是有些老版本的浏览器比如IE6的效率依然低下。
// bad
var left = 10;
var top = 10;
el.style.left = left + "px";
el.style.top = top + "px";

// better
el.className += " className";

// 当top和left的值是动态计算而成时...
// better 
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";

  1. 分离读写操作: DOM 的多个读操作(或多个写操作),应该放在一起。不要两个读操作之间,加入一个写操作。
// bad 强制刷新 触发四次重排+重绘
div.style.left = div.offsetLeft + 1 + 'px';
div.style.top = div.offsetTop + 1 + 'px';
div.style.right = div.offsetRight + 1 + 'px';
div.style.bottom = div.offsetBottom + 1 + 'px';

// good 缓存布局信息 相当于读写分离 触发一次重排+重绘
var curLeft = div.offsetLeft;
var curTop = div.offsetTop;
var curRight = div.offsetRight;
var curBottom = div.offsetBottom;

div.style.left = curLeft + 1 + 'px';
div.style.top = curTop + 1 + 'px';
div.style.right = curRight + 1 + 'px';
div.style.bottom = curBottom + 1 + 'px';

原来的操作会导致四次重排,读写分离之后实际上只触发了一次重排,这都得益于浏览器的渲染队列机制:

当我们修改了元素的几何属性,导致浏览器触发重排或重绘时。它会把该操作放进渲染队列,等到队列中的操作到了一定的数量或者到了一定的时间间隔时,浏览器就会批量执行这些操作。

  1. 将 DOM 离线: “离线”意味着不在当前的 DOM 树中做修改,我们可以这样做:

使用 display:none 一旦我们给元素设置 display:none 时(只有一次重排重绘),元素便不会再存在在渲染树中,相当于将其从页面上“拿掉”,我们之后的操作将不会触发重排和重绘,添加足够多的变更后,通过 display属性显示(另一次重排重绘)。通过这种方式即使大量变更也只触发两次重排。另外,visibility : hidden 的元素只对重绘有影响,不影响重排。

四. 判断数据类型的三种方法

1. typeof

返回一个字符串。问题:无法正确返回复杂数据类型的类型。都是'object'

2. instanceof

a instanceof A 判断a是不是A的实例,返回true/false

它可以判断出数组,函数,日期等引用类型

[] instanceof Array; // true
{} instanceof Object;// true
newDate() instanceof Date;// true
 
function Person(){};
new Person() instanceof Person;  //true

3. Object.prototype.toString.call

用对象原型上的toString方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。

对于 Object 对象,直接调用 toString() 就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。

Object.prototype.toString.call('') ;   // [object String]
Object.prototype.toString.call(1) ;    // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(Symbol()); //[object Symbol]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object global] window 是全局对象 global 的引用

五. 暂时性死区

在使用let/const声明一个变量之前,这些变量都是不可用的,这就是暂时性死区。

六. js原型链的理解

js的原型链表示了实例和它的构造函数之间的关系。每一个构造函数都自带一个prototype属性,这个属性值是一个对象,同时这个对象里有一个constructor属性,指向这个构造函数自身。由这个构造函数创建的实例对象,有一个__proto__属性,指向构造函数的prototype这个对象,这个关系就形成了原型链。当我们访问一个实例里的属性或方法,会先在实例自身上寻找,如果没有,就会去它的原型上找。构造函数的prototype这个对象就是这些实例的原型,里边保存了这些实例所共有的一些方法或属性。原型的好处就是省代码,省内存。

如一个构造函数Person,创建了实例p,那么:p.__proto__===Person.prototype 那原型的原型是什么呢?Person.prototype 也是一个对象,对象是由Object构造出来的,所以Person.prototype.__proto__===Object.prototype 。Object.prototype 就是原型链的最顶层了,Object.prototype.__proto__===null Object.prototype这个对象没有原型,指向null。

七. JS作用域

在ES6之前,没有let,没有块级作用域。只有var,全局作用域和函数作用域:

  1. 即使用花括号把var包起来,它声明的变量在外部也是能访问的。
if (true) {
    var name = 'zhangsan'
}
console.log(name)   //'zhangsan'
  1. 里面的能访问外面的,但是外部不能访问内部
function aaa(){            
    var a=10;          
};        
aaa();        
console.log(a)  //Uncaught ReferenceError: a is not defined
  1. var有变量提升,变量的声明提升到当前作用域顶部
function aaa(){            
	console.log(a);//undefined            
	var a=20;        
}        
aaa(); 

如果用let,就报错了。

var a=10;        
function aaa(){            
  console.log(a);//undefined            
  var a=20;        
}        
aaa();

里面的var a =20 ,提升到函数第一句。和外边的a无关

var a=10;        
function aaa(a){             
  console.log(a);//10            
  var a=20;  //因为 a 是形参,优先级高于 var a; 所以 局部变量a的声明其实被忽略了。        
}         
aaa(a);
  1. 变量的值取什么,就近原则
var a=10;         
function aaa(){             
    console.log(a);        
};                    
function bbb(){            
    var a=20;            
    aaa();        
}        
bbb();//10

调用aaa的时候,打印a。按照就近原则,离var a=10; 最近,所以取10.就算之前在bbb里把a赋值成了20,也不影响。

var a=10;                       
function bbb(){            
  var a=20;
  function aaa(){             
      console.log(a);
  };               
  aaa();        
}        
bbb();    //20

把函数aaa挪到bbb里边了,结果就是20,离得更近。

ES6新增了let和const,可以创建一个块级作用域。在一个块级作用域里边声明的局部变量,外边就访问不到了。这在之前是做不到的,之前只能是函数作用域里的局部变量外边访问不到。

if (true) {
 let name1 = 'zhangsan'
}
console.log(name1) // 报错,因为let定义的name是在if这个块级作⽤域

八. 前端性能优化

1. DOM方面的优化

  1. 缓存DOM
const div = document.getElementById('div')

由于查询DOM比较耗时,在同一个节点无需多次查询的情况下,可以缓存DOM

  1. 减少DOM深度及DOM数量

  HTML 中标签元素越多,标签的层级越深,浏览器解析DOM并绘制到浏览器中所花的时间就越长,所以应尽可能保持 DOM 元素简洁和层级较少。

  1. 减少DOM操作次数,批量操作DOM

  由于DOM操作比较耗时,且可能会造成回流,因此要避免频繁操作DOM,可以批量操作DOM,先用字符串拼接完毕,再用innerHTML更新DOM

  1. DOM元素离线更新

  对DOM进行相关操作时,例、appendChild等都可以使用Document Fragment对象进行离线操作,带元素“组装”完成后再一次插入页面,或者使用display:none 对元素隐藏,在元素“消失”后进行相关操作

  1. DOM读写分离

  浏览器具有惰性渲染机制,连接多次修改DOM可能只触发浏览器的一次渲染。而如果修改DOM后,立即读取DOM。为了保证读取到正确的DOM值,会触发浏览器的一次渲染。因此,修改DOM的操作要与访问DOM分开进行

  1. 事件代理

  事件代理是指将事件监听器注册在父级元素上,由于子元素的事件会通过事件冒泡的方式向上传播到父节点,因此,可以由父节点的监听函数统一处理多个子元素的事件。利用事件代理,可以减少内存使用,提高性能及降低代码复杂度

  1. 防抖和节流

  使用函数节流(throttle)或函数去抖(debounce),限制某一个方法的频繁触发

2. 减少http请求

把JS CSS 图片文件进行压缩,合并

合理使用http缓存:使用cache-control或expires这类强缓存时,缓存不过期的情况下,不向服务器发送请求。强缓存过期时,会使用last-modified或etag这类协商缓存,向服务器发送请求,如果资源没有变化,则服务器返回304响应,浏览器继续从本地缓存加载资源;如果资源更新了,则服务器将更新后的资源发送到浏览器,并返回200响应

图片懒加载

3. CSS方面的优化

  1. 避免使用 CSS 表达式

  2. 不使用CSS @import,会造成额外的请求,使用<link>代替

  3. CSS使用标签放在顶部,JS放在底部

九. 移动端touch相关事件

touchstart touchmove touchend 三个常用的事件

其他概念:触摸事件touchevent,有三个属性:TouchEvent.touches TouchEvent.targetTouches TouchEvent.changedTouches 他们三个都是一个TouchList对象,就是代表多个触点的一个列表。

TouchEvent.changedTouches: 一个 TouchList 对象,包含了代表所有从上一次触摸事件到此次事件过程中,状态发生了改变的触点的 Touch 对象。

TouchEvent.targetTouches: 一个 TouchList 对象,是包含了如下触点的 Touch 对象:触摸起始于当前事件的目标 element 上,并且仍然没有离开触摸平面的触点。

TouchEvent.touches: 一 个 TouchList 对象,包含了所有当前接触触摸平面的触点的 Touch 对象,无论它们的起始于哪个 element 上,也无论它们状态是否发生了变化。

参考文章:移动端开发教程-移动端事件

十. 把数组按父子关系转换成树型结构

const 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},
    {id: 9, name: 'I', parentId: 5}
];
//转换成下面的形式
//const tree = [
//    {
//        id: 1,
//        name: 'A',
//        parentId: 0,
//        children: [
//            {
//                id: 3,
//                name: 'C',
//                parentId: 1,
//                children: [...]
//            }
//        ]
//    },
//    ...
//

paerntId是0的对象,是最上层的。用children属性表示它的孩子。孩子再嵌套孩子。

function toTree(list){
  list.forEach(item=>{
    let parentId=item.parentId
    if(parentId!==0){
      list.forEach(el=>{
        if(el.id===parentId){
          if(!el.children){
            el.children=[]
          }
          el.children.push(item)
        }
      })
    }
  })
  list.filter(el=>el.parentId===0)
  return list
}

双重循环,先遍历所有项,parentId是0的什么也不做,不是0的,就说明它是某个项的孩子,就先拿到它的parentId,然后再遍历所有项,找到它的parent,放进children数组里。遍历完之后,parentId是0的项,里边的children数组就保存了所有父子结构,其余不是0的就可以去掉了。因为放进children数组里的是对象,是引用类型,所以如果2是1的孩子,3又是2的孩子,3被放进了2的children里,这个链的结构也都保存在了1的children里。