大web之js高频

251

1. 实现new方法

在new的过程中,发生以下四个过程

    1. 创建一个新对象(若该函数不是js内置的,则创建一个新的Object对象)
    1. 将这个空对象的__proto__ 属性指向构造函数的原型对象(链接到原型 实现继承)
    1. 绑定this,执行构造函数中的代码(为这个新对象添加属性)
    1. 返回一个新对象(若函数没有返回其他对象,则自动返回这个新对象;若函数有return返回的是非对象,则还是自动返回这个新对象,覆盖那个非对象)
function _new() {
    // 1. 创造一个新对象
    const obj = {}
    // 2. 绑定原型
    let [constructor,...args] = [...arguments]
    obj.__proto__ = constructor.prototype
    // 3. 绑定this
    const result = constructor.apply(obj, args)
    // 4. 返回新对象
    return Object.prototype.toString.call(result) == '[object Object]' ? result : obj;
}
// 使用
    function People(name,age) { 
       this.name = name
       this.age = age 
     }
   let peo = createNew(People,'Bob',22) 
   console.log(peo.name) 
   console.log(peo.age)

3. 实现一个call函数

通过传入一个对象(若为基本类型,会被封装函数转化为对象---装箱),将this绑定到该对象。

// 首先上一个call的使用
function add(c,d) {
    return this.a + this.b + c + d
}
const obj = { a: 1, b: 2}

add.call(obj, 3, 4) // 10
add.apply(obj, [3, 4]) // 10

call作用就是改变this指向,……,实际发生过程大概就是下面的样子

    1. 给obj添加一个add属性,这时候this就是指向obj了
    1. obj.add(3,4)的结果和add.call(obj, 3,4)相同
    1. 最后将多添加的add属性delete掉

基于ES3实现call

Function.prototype.es3Call = function(context) {
    // context 形参就一个,形参为空时,context就赋值window,否则就是传入的值
    context = context ||  window
    // 谁调用谁就是this
    context.fn = this
    var args = []
    // argunments 入参伪数组 下面是过滤出第一个对象,push进其他参数
    for (var i= 1; len = argunments.lenght; i < len; i++) {
        args.push('arguments['+i+']')
    }
    // eval()方法,会对传入的字符串当做js代码进行解析
    var result = eval('context.fn('+args+')')
    // args 会自动调用 args.toString() 方法,因为'context.fn(' + args +')'本质上是字符串拼接,会自动调用toString()方法
    delete context.fn
    return result
}

基于ES6实现,更简便

Function.prototype.es6Call = function(context) {
    context = context || window
    context.fn = this
    let args = [...arguments].slice(1);
    // 返回执行后的结果
    const result = context.fn(...args)
    delete context.fn
    return result
}

4. 实现apply方法

apply的作用和call一样,只是接受的额外参数形式不一样,apply接受的额外参数必须是数组

    Function.prototype.apply = function(context,arr) {
        context = context ? Object(context) : window;
        context.fn = this

        var result;
        if (!arr) {
            result = context.fn();
        } else {
        // <!-- ES3 -->
            result = eval('context.fn('+arr+')')
        //  <!-- ES6 -->
            result =  context.fn(...arr) 
        }
        delete context.fn
        return result
    }

5. 实现一个bind函数

bind与call和apply的区别在于,call和apply返回的是执行的结果,而bind返回的是一个函数

Function.prototype.bind = function(context) {
    if (typeof this !== 'function') {
        throw new TypeError('Error')
    }
    let fn = this
    let arg = [...arguments].slice(1)
    return function F() {
        // 处理函数使用new的情况
        ifthis instanceof F) {
            return new fn(...arg, ...arguments)
        } else {
            return fn.apply(context, arg.concat(...arguments))
        }
    }
    
}
复制代码

6. call、apply和bind的区别和应用

基本语法

fun.call(thisArg, param1, param2, ...)
fun.apply(thisArg, [param1, param2, ...])
fun.bind(thisArg, param1, param2, ...)

返回值

call() 和apply() 返回函数应该返回的值,bind()返回一个经过apply或者call硬绑定(显示绑定)后的的新函数

执行

call()和apply()一经调用立即执行,而bind()则只是完成了函数this的绑定。因为函数不会立刻执行,所以适合在事件绑定函数中使用bind(),这样既完成了绑定,也确保了仅当事件触发时才会执行

应用场景

call(), apply()和bind()都可以改变this指向,什么时候需要改变this指向呢?大部分的时候其实是为了借用方法,即在对象上调用其自身并不具备的方法,下面是一一例子

1. 判断数据类型

Object.prototype.toString.call() 可以准确判断数据类型

var a = "abc";
var b = [1,2,3];
Object.prototype.toString.call(a) == "[object String]" //true
Object.prototype.toString.call(b) == "[object Array]"  //true

原理就是,在任何值上调用Object原生的toString()方法,都会范湖一个格式为[object NativeconstructorName]的字符串,据此可以准确判断任何值的数据类型。 既然Array和Function都继承了Object的该方法,为什么不直接在他们身上调用?这是因为toString()被重写过了,不是原生方法因此这里改为调用Object的该方法,并将this绑定给对应的值。

2. 模拟浅拷贝

模拟浅拷贝的过程中,需要剔除原型链上的属性,考虑到源对象可能是基于Object.create()创建,而这样的对象没有hasOwnPrototype()方法的,因此我们不在源对象上直接调用该方法,而是通过Object.prototype.hasOwnPrototype.call()的方式去调用,因为Object一定有这个方法的,所以我们可以借用一下

if (Object.prototype.hasOwnPrototype.call(nextSource,nextKey)) {
    to[nextKey] = nextSource[nextKey]
}

3. 继承

JavaScript的几种继承方式中,有一种就是借用构造函数 假设有子构造函数Son,和父构造函数Paren。 对于Son而言,其内部的this指向稍后实例化的对象,利用这一点,我们在Son内部通过call或者apply函数,调用parent,同时传参this,这样就可以通过增强子类实例

4. 类数组借用数组的方法

例如arguments是类数组,并不具备数组的forEach()方法,我们可以通过call()调用数组的该方法,同时将方法中的this绑定到arguments上

Array.prototype.forEach.call(arguments,item => {
    console.log(item)
})

5. 求数组的最值

核心是apply可用于展开数组,既我们前面所说的将参数数组转化为参数列表 例如我们可以求一个数组的最大值,虽然Math对象有max()方法,但是该方法只接受参数列表,那么这时候可以通过apply去调用该方法,从而展开数组 Math.max.call(null, arr)

7. 浅拷贝和深拷贝的实现

浅拷贝

// 方法一
let copy1 = { ... {x: 1}}

// 方法二
let copy2 = Object.assign({}, {x:1})

深拷贝

// 方法一: 缺点L拷贝对象如果包含正则表达式,函数或者undefined等值时会失败
JSON.parse(JSON.strigify(obj))

// 方法二: 递归拷贝
function deepClone(obj) {
    let copy = Obj instanceif Array ? [] : {}
    for (let i in obj) {
        if (obj.hasOwnProperty(i)) {
            copy[i] = typeof Obj[i] === 'object' ? deepClone(obj[i]) : obj[i]
        }
    }
    return copy
}

8. 节流和防抖

肥仔播客

前言

以下场景往往由于事件频繁被触发,因而频繁执行DOM操作、资源加载等重行为,导致UI停顿甚至浏览器崩溃

    1. window对象的resize,scroll事件
    1. 拖拽时的mousemove事件
    1. 射击游戏中的mousedown,keydown事件
    1. 文字如、自动完成的keyup事件 实际上,对于window的resize事件,实际需求大多数为停止改变大小n毫秒后执行后续处理; 而其他事件大多数的需求是在以一定的频率执行后续处理.

什么是debounce

1. 定义

如果用手一直按住一个弹簧,它将不会弹起直到你松手为止 也就是说当调用n毫秒后,才会执行该动作,若在这n毫秒内又调用此动作将重新计算执行时间(在规定时间内未触发第二次,则执行) 接口定义

/**
 * 空闲控制,返回函数连续调用时,空闲时间必须》=idle, action才会执行
 * @param delay 空闲时间
 * @param fn 实际应用需要调用的函数
 * @return 返回客服调用函数
*/

简单实现

const debounce = (fn, delay) {
    // 利用闭包保存定时器
    let timer
    return function() {
        // 在规定时间内再次被触发会清除定时器后再重新设定定时器
        clearTimeout(timer)
        timer = setTimeout(() => {
            fn.apply(this, arguments)
        }, delay)
    }

}

function fn() {
    console.log('防抖')
}
addEventListener('scroll', debounce(fn,1000))

什么是节流

在规定时间内置触发一次(和拧紧的水龙头的水 ,一定时间内只滴一滴)

function throttle(fn,delay) {
    // 利用闭包保存时间
    let prev = Date.now()
    return function(){
        const now = Date.now()
        if (now - prev >= delay) {
            fn.apply(this, arguments)
            prev = Date.now()
        }
    }
}

function fn () { console.log('节流') }
addEventListener('scroll', throttle(fn, 1000))

9. instanceof的原理

就是右边变量的原型存在于左边变量的原型链上

function instanceOf(left, right) {
    let leftValue = left.__proto__ // 取隐式原型
    let rightValue = right.prototype //  取显式原型
    white(true) {
        if (leftValue === null) {
            return false
        }
        // 当右边的显式原型严格等于左边的隐式原型时,返回true
        if (leftValue ==== rightValue) {
            return true
        }
        leftValue = leftValue.__proto__
    }
}

10. 柯里化函数的实现

定义: 将多参的函数转换成单参数的形式 原理:利用闭包原理在执行可以形成一个不销毁的作用域,然后把需要预先处理的内容都储存在这个不销毁的作用域中,并且返回一个最少参数的函数。

第一种:固定传入参数,参数够了才执行

/**
* 实现要点: 柯里化函数接收到足够参数后,就会执行原函数,那么我们如何去确定何时达到足够的参数呢?
* 柯里化函数需要记住你已经给他的参数,如果没有给的话,则默认一个空数组
* 接下来每次调用的时候,需要检查参数是否给够,如果给够了,则执行fn,没有的话则返回一个新的curry函数,将现有的参数塞给它
*/
// 待柯里化处理的函数
let sum = (a, b, c, d) => {
    return a + b + c + d
}

// 柯里化函数,返回一个被处理过的函数
let curry = (fn, ...arr) => {// arr记录已有参数
    console.log(arr)
    const fnParamsLen = fn.length
    return (...args) => { // args 接收新参数
        if (fnParamsLen <= [...arr, ...args].length) { // 参数足够时,触发执行
            return fn(...arr, ...args)
        } else { // 继续添加参数
            return curry(fn, [...arr, ...args])
        }
    } 

}
var sumPlus = curry(sum)
sumPlus(1)(2)(3)(4)
sumPlus(1, 2)(3)(4)
sumPlus(1, 2, 3)(4)

第二种: 不固定传入参数,随时执行

/**
    * 当然了,柯里化的主要作用还是延迟执行,执行的触发条件不一定是参数个数相等,也可以是其它条件,例如最后接受的参数个数为0的情况,那么我们需要对上面curry函数稍微做修改
*/
// 待柯里化处理的函数 
let sum = arr => { 
    return arr.reduce((a, b) => { return a + b }, []) 
}
let curry = (fn, ...arr) => {
    return (...args) => {
        if (args.length === 0) {
            return fn(...arr, ...args)
        } else {
            return curry(fn,...arr, ...args)
        }
    }
}

11. Object.cteate的基本实现原理

**方法说明: **

  1. Object.create()方法创建一个新的对象,并以方法的第一个参数作为新的对象的__proto__的属性的值(以第一个参数作为新对象的构造函数的原型对象);
  2. Object.create()方法还有第二个可选参数,是一个对象,对象的每个属性都会作为新对象的自身属性,对象的属性值以descriptor(Object.getOwnPropertyDescriptor(obj,'key'))的形式出现,且enumenrable默认为false 源码点拨 搞清楚上面的方法描述说的意思,然后实现的时候其实是比较简单的,就是定义一个空的构造函数,然后指定构造函数的原型对象,通过new运算符创建一个空对象,如果发现传递了第二个参数,通过Object.defineProperties为创建的对象设置key,value,最后返回创建的对象即可。 源码:原文链接
Object.myCreate = function(proto, propertyObject = undefined) {
    if (propertyObject === null) {
        // 这里没有判断propertyObject是否是原始包装对象
        throw 'TypeError'
        return
    }
    // 定义一个空的构造函数
    function Fn() {}
    // 指定构造函数的原始对象
    Fn.prototype = proto
    // 通过new运算符创建一个空对象
    const obj = new Fn()
    // 如果第二个参数不为空
    if (propertyObject !== undefined) {
        Object.defineProperties(obj, propertyObject)
    }
    if(proto === null) {
        // 创建一个没有原型对象的对象,Object.create(null)
        obj.__proto__ = null
    }
    return obj
}
// 示例
// 第二个参数为null时,抛出TypeError
// const throwErr = Object.myCreate({a: 'aa'}, null)  // Uncaught TypeError
// 构建一个以
const obj1 = Object.myCreate({a: 'aa'})
console.log(obj1)  // {}, obj1的构造函数的原型对象是{a: 'aa'}
const obj2 = Object.myCreate({a: 'aa'}, {
  b: {
    value: 'bb',
    enumerable: true
  }
})
console.log(obj2)  // {b: 'bb'}, obj2的构造函数的原型对象是{a: 'aa'}

12. 实现一个基本的Event Bus

EventBus的作用就是作为一个中间件,是链接两个组件的一座桥梁

  • 发送方通过EventBusName.$emit('eventName', data)将数据和事件名传递给EventBus
  • 接收方则通过EventBusName.$on('eventName', methods)对数据进行处理

简单实现

class EventBus {
    constructor() {
        // 存储时事件
        this.events = this.events || new Map()
    }
    
    // 监听事件
    addListener(type, fn) {
        if (!this.events.get(type)) {
            this.events.set(type, fn)
        }
    }
    
    // 触发事件
    emit(type) {
        let handle = this.events.get(type)
        handle.apply(this,[...arguments].slice(1))
    }
}

// 测试
let emitter = new EventBus()
// 监听事件
emitter.addListener('add', age => {console.log(age)})
// 触发事件
emitter.emit('add', 18)

升级改造

如果重复监听同一个事件名呢? 上述在绑定第一个坚挺着之后就无法对后续其他监听者进行注册绑定了,因为我们需要将后续监听者放入到一个数组中

Class EventBus {
    constructor() {
        // 存储时事件
        this.events = this.events || new Map()
    }
    
    // 监听事件
    addListener(type, fn) {
        const handler = this.events.get(type)
        if (!handler) {
            this.events.set(type, fn)
        } else if(handler && typeof handler ==== 'function'){
            // 如果handler是函数说明在之前只有一个监听者
            this._events.set(type, [handler, fn]); // 多个监听者我们需要用数组储存
        } else {
            handler.push(fn); // 已经有多个监听者,那么直接往数组里push函数即可
        }
    }
    
    // 触发事件
    emit(type,...args) {
        const handler = this.events.get(type)
        // 如果是一个数组说明有多个监听者,需要依次触发里面的函数
        if (Array.isArray(handler)) {
            for (let i = 0; i < handler.lenght; i++) {
                handler[i].apply(this, args)
            }
        } else {
            // 单个函数的情况我们直接触发即可
            handler.apply(this, args);
        }
    }
    
    // 移除监听用removeListener
    removeListener(type, fn) {
        const handler = this._events.get(type); // 获取对应事件名称的函数清单
        // 如果是函数,说明只被监听了一次
         if (handler && typeof handler === 'function') {
           this._events.delete(type, fn);
         } else {
            let postion;
             // 如果handler是数组,说明被监听多次要找到对应的函数
            for (let i = 0; i < handler.length; i++) {
               if (handler[i] === fn) {
                 postion = i;
               } else {
                postion = -1;
               }
            }
            // 如果找到匹配的函数,从数组中清除
            if (postion !== -1) {
              // 找到数组对应的位置,直接清除此回调
              handler.splice(postion, 1);
              // 如果清除后只有一个函数,那么取消数组,以函数形式保存
              if (handler.length === 1) {
                this._events.set(type, handler[0]);
              }
            } else {
              return this;
            }
           }
         }
    }
}

13. 实现一个双向数据绑定

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
})

更详细的见这应该是最详细的响应式系统讲解了

14. 实现一个简单的路由

原理:以 hash 形式(也可以使用 History API 来处理)为例,当 url 的 hash 发生变化时,触发 hashchange 注册的回调,回调中去进行不同的操作,进行不同的内容的展示。

class Route {
    constructor() {
        // 路由存储对象
        this.routes = {}
        // 当前hash
        this.currentHash = ''
        // 绑定this,避免监听时this指向改变 this.freshRoute = this.freshRoute.bind(this)
        window.addEventListener('load', this.refresh.bind(this), false)
        window.addEventListener('hashchange', this.freshRoute.bind(this), false)
    }
     // 更新 
     freshRoute () { 
         this.currentHash = location.hash.slice(1) || '/' 
         this.routes[this.currentHash]() 
     }
     // 存储或者说新增
     storeRoute (path, cb) { 
         this.routes[path] = cb || function () {} 
     }
}

15. 实现懒加载

<ul>
    <li><img src="./imgs/default.png" data="./imgs/1.png" alt=""></li>
    <li><img src="./imgs/default.png" data="./imgs/2.png" alt=""></li> 
    <li><img src="./imgs/default.png" data="./imgs/3.png" alt=""></li> 
    <li><img src="./imgs/default.png" data="./imgs/4.png" alt=""></li> 
    <li><img src="./imgs/default.png" data="./imgs/5.png" alt=""></li> 
    <li><img src="./imgs/default.png" data="./imgs/6.png" alt=""></li>
    <li><img src="./imgs/default.png" data="./imgs/7.png" alt=""></li> 
    <li><img src="./imgs/default.png" data="./imgs/8.png" alt=""></li>
    <li><img src="./imgs/default.png" data="./imgs/9.png" alt=""></li>
    <li><img src="./imgs/default.png" data="./imgs/10.png" alt=""></li>
</ul>

初始时图片的src加载显示的均为默认图片。 实际图片放在img标签的attribute的data上 原理: 监听屏幕滚动,计算图片位置,当图片在屏幕可视区域内时,就将图片的src替换成实际要显示的资源。 所以关键在于要知道图片是否在可视区域内(遍历图片,求出临界点,刚进入以及刚好完全出去)

// 可视区域高度不同浏览器兼容写法(后两个Internet Explorer 8、7、6、5:的写法)
let clientHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;

// 获取所有图片的节点
const imgs = document.querySelectorAll('img')
// 监听屏幕滚动时,进行懒加载计算
window.addEventListener('scroll', lazyLoad)

// 懒加载
function lazyLoad () {
    // 滚动卷出去的高度
    let scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop
    // 遍历图片节点,确定图片出现在可视区域内的赋值src
    for (let i = 0; i < imgs.length; i++) {
        // 寻找当前图片的顶角距离外层元素的距离
        const curImgOffsetTop = imgs[i].offsetTop
        // 寻找临界点,刚好要滚动进入,以及刚好完全滚出(均以外层元素顶点为参考点)
        // 刚好要滚动进入的点
        const readyInOffestTop = scrollTop + clientHeight
        // 刚好完全滚出的点(刚好出去时,图片的左上角距离顶部的距离)
        const justOutOffestTop = scrollTop - imgs[i].height
        // 出现在可视区的
        if (curImgOffsetTop > readyInOffestTop && curImgOffsetTop <  justOutOffestTop ){
            imgs[i].src = imgs[i].getAttribute('data')
        }
       
    }
}

优化找到第一个在屏幕之下的图片的index,小于该index的更新src

16. 移动端适配rem

理解:

  • em是相对于父元素字体大小的大小的单位,当父元素是30px,子元素为0.8em,则子元素就是30 * 0.8 = 24px;
  • rem则是相对于html艮元素的大小单位,当
<html lang="zh-cmn-Hans" style="font-size: 50px;">
    <body style="font-size: 0">
        <div class="box1">这是div盒子</div>
    <body>
</html>
.box1 {
    font-size: 6rem
}
``
则经过渲染后,实际box的大小将会变成`50px * 6 = 300px`; 可以理解成,rem 就是 50px
所以移动端适配关键节点在于确定rem到底是多少px;
我们拿到设计图开发时,需要注意两个,一个是`不同尺寸的手机`,第二个是`设计只会给一个标准的设计稿`,
这时我们还希望我们看着设计稿就能很轻松的写rem。
现在假装我们的根元素的fontSize设置成了x(px),然后我希望把box1的宽设置成全屏宽, 比方设计给的标准宽就是750,高是1334; 我希望我后续写的时候只用关注设计稿。 然后我就希望下面是成立的:
```html
<div :style="width: `${designWidth}rem`;">这是div盒子</div>
    const designWidth = 750
    // 设备呢,宽度要求
    const clientWidth = document.documentElement.clientWidth || document.body.clientWidth || window.innerWidth || window.screen.width
    designWidth + rem = designWidth * x = clientWidth
    // 解方程就可以得到
    x = clientWidth / designWidth
   

所以设置html根元素的fontSize正确姿势就是:

document.documentElement.style.fontSize = ( clientWidth / designWidth) + 'px';

然后再实际工作中,我们会遇到横屏或者竖屏, 这时designWidth就是变化的了;这时可以通过路由拦截meta中的值,动态设置 再者就是当在pc端屏幕大小可以改变时,就要监听resize,改变根元素的fontSize了

addEventListener("resize", setHtmlRootFontSize)