JavaScript基础-手写代码

148 阅读7分钟

JavaScript技巧代码片段

使用navigator对象获取媒体设备相关数据

navigator.mediaDevices(); // 返回可用的媒体设备
navigator.getUserMedia(); // 返回可用媒体设备硬件关联的流

使用typeof判断一个变量是否存在

typeof操作符返回一个字符串,表示未经计算的操作数的类型:

if(typeof a != 'undefined'){
    // 变量存在
}

不能使用if(a),若a未声明,则报错

instanceof的实现原理

function myInstanceof(left,right){
    // 这里先用typeof判断基础数据类型,如果是,直接返回false
    if(typeof left !== 'object' || left === null) {
        return false
    }
    // getProtypeOf是Object对象自带的API,能够拿到参数的原型对象
    let proto = Object.getProtypeOf(left);
    while(true) {
        if(proto === null) return false;
        if(proto === right.prototype) return true;//找到相同原型对象,返回true
        proto = Object.getPrototypeof(proto);
    }
}

顺着原型链去找,直到找到相同的原型对象,返回true,否则为false

typeof 与 instanceof 区别

typeof instanceof都是判断数据类型的方法,区别如下:

  • typeof 会返回一个变量的基本类型,instanceof返回的是一个布尔值
  • instanceof 可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型
  • typeof也存在弊端,它虽然可以判断基础数据类型(null除外),但是引用数据类型中,除了function 类型以外,其他的也无法判断。 如果需要通过检测数据类型,可以采用Object.prototype.toString,调用该方法,统一返回格式“[object Xxx]”的字符串。 下面为一个全局通用的数据类型判断方法。
function getType(obj){
    let type = typeof obj;
    // 先进行typeof判断,如果是基础数据类型,直接返回
    if(type !== "object"){
        return type;
    }
    //对于typeof返回结果是object,再进行如下的判断,正则返回结果
    return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/,'$1')
}

手写new操作符

new关键字工作流程:

  • 创建一个新的对象obj
  • 将对象与构建函数通过原型链连接起来
  • 将构建函数中的this绑定到新建的对象obj上
  • 根据构建函数返回类型作判断,如果是原始值则被忽略,如果返回对象,需要正常处理
function mynew(Func,...args){
    // 1.创建一个新对象
    const obj = {};
    // 2.新对象原型指向构造函数原型对象
    obj.__proto__ = Func.prototype
    // 3.将构建函数的this指向新对象
    let result = Func.apply(obj,args)
    // 4.根据返回值判断
    return result instanceof Object ? result : obj
}

bind,call,apply 手写bind

实现bind的步骤,分解成三步

  • 修改this指向
  • 动态传递参数
//方式1:只在bind中传递函数参数
fn。bind(obj,1,2)()

//方式2:在bind中传递函数参数,也返回函数中传递参数
fn.bind(obj,1)(2)
  • 兼容new关键字 代码如下:
Function.prototype.myBind = function(context){
    // 判断调用对象是否为函数
    if(typeof this !== "function"){
        throw new TypeError("Error")
    }
    
    //获取参数
    const args = [...arguments].slice(1),
        fn = this;
    return function Fn(){
    
        //根据调用方式,传入不同绑定值
        return fn.apply(this instanceof Fn ? new fn(...arguments) : context,args.concat(...arguments));
    }
}

闭包柯里化函数

闭包

闭包是一个函数和对周围状态(词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包。 闭包让你可以在一个内层函数中访问到其外层函数的作用域。 闭包使用场景:

  • 创建私有变量
  • 延长变量的生命周期

一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的。

柯里化函数

柯里化的目的在于避免频繁调用具有相同参数函数的同时,又能够轻松的重用。

//假设我们又一个求长方形面积的函数
function getArea(width,height){
    return width*height
}
//如果我们碰到长方形的宽老是10 
const area1 = getArea(10,20)
const area2 = getArea(10,30)
const area3 = getArea(10,40)

//我们可以使用闭包柯里化这个计算面积的函数
function getArea(width){
    return height => {
        return width*height
    }
}

const getTenWidthArea = getArea(10)
//之后碰到宽度为10的长方形就可以这样计算面积
const area1 = getTenWidthArea(20)

//而且如果遇到宽度偶尔变化也可以轻松复用
const getTwentyWidthArea = getArea(20)

使用闭包模拟私有方法

在JavaScript中,没有支持声明私有变量,但我们可以使用闭包来模拟私有方法

var Counter = (function(){
    var privateCounter = 0;
    function changeBy(val){
        privateCounter +=val;
    }
    return {
        increment:function(){
            changeBy(1);
        },
        decrement:function(){
            changeBy(-1);
        },
        value:function(){
            return privateCounter;
        }
    }
})();

var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */

上述通过使用闭包来定义公共函数,并令其可以访问私有函数和变量,这种方式也叫模块方式。 两个计数器Counter1和Counter2是维护它们各自的独立性的,每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境,不会影响另一个闭包中的变量。

其他注意

例如计数器、延迟调用、回调等闭包的应用,其核心思想还是创建私有变量和延长变量的生命周期。 如果某些特定任务需要使用闭包,在其他函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响 例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因在于每个对象的创建,方法都会被重新赋值。

function MyObject(name,message){
    this.name = name.toString();
    this.message = message.toString();
    this.getName = function(){
        return this.name;
    }
    
    this.getMessage = function(){
        return this.message;
    }
}

上面的代码中,并没有利用到闭包的好处,因此可以避免使用闭包。修改如下:

function MyObject(name,message){
    this.name = name.toString();
    this.message = message.toString();
}
MyObject.prototype.getName = function(){
    return this.name;
}
MyObject.prototype.getMessage = function(){
    return this.message;
}

手写代码实现浅拷贝

浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝。 如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址。 即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址 下面实现简单浅拷贝。

function shallowClone(obj){
    const newObj = {};
    for(let prop in obj){
        if(obj.hasOwnProperty(prop)){
            newObj[prop] = obj[prop];
        }
    }
    return newObj;
}

手写代码实现深拷贝

深拷贝开辟一个新的栈,两个对象属性完全相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。 常见的深拷贝方式有:

  • _.cloneDeep()
  • jQuery.extend()
  • JSON.string()
  • 手写循环递归 WeakMap:WeakMap 对象是一组键值对的集合,其中的键是弱引用对象,而值可以是任意
function deepClone(obj,hash = new WeakMap){
    if(obj === null)return obj;//如果是null或者undefined 则不进行拷贝操作
    if(obj instanceof Date) return new Date(obj);
    if(obj instanceof RegExp) return new RegExp(obj);
    //可能是对象或者普通的值,如果是函数的话是不需要深拷贝。
    if(typeof obj!== "object") return obj;
    //是对象的话才进行深拷贝
    if(hash.get(obj)) return hash.get(obj); 
    let cloneObj = new obj.constructor();
    //找到的是所属类原型上的constructor,而原型上的consturctor指向的是当前类本身
    hash.set(obj,cloneObj);
    for(let key in obj){
        if(obj.hasOwnProperty(key)){
            //实现一个递归拷贝
            cloneObj[key] = deepClone(obj[key],hash);
        }
    }
    return cloneObj;
}

手写节流与防抖

节流

定义:n秒内只运行一次,若在n秒内重复触发,只有一次生效。

function throttled1(fn,delay = 500){
    let oldtime = Date.now()
    return function(...args){
        let newtime = Date.now()
        if(newtime - oldtime >= delay){
            fn.apply(null,args)
            oldtime = Date.now()
        }
    }
}

使用定时器写法,delay毫秒后第一次执行,第二次事件停止触发后,依然会再一次执行。

funtion throttled2(fn,delay = 500){
    let timer = null
    return function (...args){
        if(!timer){
            timer = setTimeout(()=>{
                fn.apply(this,args)
                timer = null
            },delay);
        }
    }
}

可以将时间戳写法的特性与定时器写法的特性相结合,实现一个更加精确的节流。实现如下:

function throttled(fn,delay){
    let timer = null
    let starttimer = Date.now()
    return function () {
        let curtime = Date.now() //当前时间
        let remaining = delay - (curTime - starttime) //从上一次到现在,还剩下多少多余时间
        let context = this
        let args = argumnents
        clearTimeout(timer)
        if(remaining <= 0){
            fn.apply(context,args)
            starttime = Date.now()
        }else{
            timer = setTimeout(fn,remaining);
        }
    }
}