热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

你所不知道的JavaScript

JavaScript栏目介绍一些必会操作。

Javascript栏目介绍一些必会操作。

和其他“圈子”里的同学们不一样,前端圈子里的同学们都很热衷于“手写xxx方法”,基本上每天在掘金里都可以看到类似的文章。但是,很多文章(不代表全部,无意冒犯)大都是囫囵吞枣、依葫芦画瓢,经不起推敲和考究,很容易误导那些对Javascript刚入门的新同学。

鉴于此,本文将基于《你不知道的Javascript》(小黄书)里一些典型的知识点,结合一些经典的、高频的被“手写”的方法来逐一地原理和实现相结合,和同学们一起在搞懂原理的基础上再去手写代码。

一、操作符new

在讲解它之前我们首先需要澄清一个非常常见的关于 Javascript 中函数和对象的误解:

在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用 new 初始化类时会调用类中的构造函数。通常的形式是这样的:

something = new MyClass(..); 

Javascript 也有一个 new 操作符,使用方法看起来也和那些面向类的语言一样,绝大多数开发者都认为 Javascript 中 new 的机制也和那些语言一样。然而,Javascript 中 new 的机制实际上和面向类的语言完全不同。

首先我们重新定义一下 Javascript 中的“构造函数”。在 Javascript 中,构造函数只是一些使用 new 操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被 new 操作符调用的普通函数而已。

实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。 使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:

  1. 创建(或者说构造)一个全新的对象;
  2. 这个新对象会被执行 [[ 原型 ]] 连接;
  3. 这个新对象会绑定到函数调用的 this ;
  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。

因此,如果我们要想写出一个合乎理论的 new ,就必须严格按照上面的步骤,落实到代码上就是:

/**
* @param {fn} Function(any) 构造函数
* @param {arg1, arg2, ...} 指定的参数列表
*/
function myNew (fn, ...args) {
    // 创建一个新对象,并把它的原型链(__proto__)指向构造函数的原型对象
    const instance = Object.create(fn.prototype)

    // 把新对象作为thisArgs和参数列表一起使用call或apply调用构造函数
    const result = fn.apply(instance, args)

    如果构造函数的执行结果返回了对象类型的数据(排除null),则返回该对象,否则返新对象
    return (result && typeof instance === 'object') ? result : instance
}  
 

示例代码中,我们使用Object.create(fn.prototype)创建空对象,使其的原型链__proto__指向构造函数的原型对象fn.prototype,后面我们也会自己手写一个Object.create()方法搞清楚它是如何做到的。

二、操作符instanceof

在相当长的一段时间里,Javascript 只有一些近似类的语法元素,如newinstanceof,不过在后来的 ES6 中新增了一些元素,比如 class 关键字。

在不考虑class的前提下,newinstanceof之间的关系“暧昧不清”。之所以会出现newinstanceof这些操作符,其主要目的就是为了向“面向对象编程”靠拢。

因此,我们既然搞懂了new,就没有理由不去搞清楚instanceof。引用MDN上对于instanceof的描述:“instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上”。

看到这里,基本上明白了,instanceof的实现需要考验你对原型链和prototype的理解。在Javascript中关于原型和原型链的内容需要大篇幅的内容才能讲述得清楚,而网上也有一些不错的总结博文,其中帮你彻底搞懂JS中的prototype、__proto__与constructor(图解)就是一篇难得的精品文章,通透得梳理并总结了它们之间的关系和联系。

《你不知道的Javascript上卷》第二部分-第5章则更基础、更全面地得介绍了原型相关的内容,值得一读。

以下instanceof代码的实现,虽然很简单,但是需要建立在你对原型和原型链有所了解的基础之上,建议你先把以上的博文或文章看懂了再继续。

/**
* @param {left} Object 实例对象
* @param {right} Function 构造函数
*/
function myInstanceof (left, right) {
    // 保证运算符右侧是一个构造函数
    if (typeof right !== 'function') {
        throw new Error('运算符右侧必须是一个构造函数')
        return
    }

    // 如果运算符左侧是一个null或者基本数据类型的值,直接返回false 
    if (left === null || !['function', 'object'].includes(typeof left)) {
        return false
    }

    // 只要该构造函数的原型对象出现在实例对象的原型链上,则返回true,否则返回false
    let proto = Object.getPrototypeOf(left)
    while (true) {

        // 遍历完了目标对象的原型链都没找到那就是没有,即到了Object.prototype

        if (proto === null) return false

        // 找到了
        if (proto === right.prototype) return true

        // 沿着原型链继续向上找
        proto = Object.getPrototypeOf(proto)
    }
} 

三、 Object.create()

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

在《你不知道的Javascript》中,多次用到了Object.create()这个方法去模仿传统面向对象编程中的“继承”,其中也包括上面讲到了new操作符的实现过程。在MDN中对它的介绍也很简短,主要内容大都在描述可选参数propertiesObject的用法。

简单起见,为了和newinstanceof的知识串联起来,我们只着重关注Object.create()的第一个参数proto,咱不讨论propertiesObject的实现和具体特性。

/**
* 基础版本
* @param {Object} proto
*  
*/
Object.prototype.create = function (proto) {  
    // 利用new操作符的特性:创建一个对象,其原型链(__proto__)指向构造函数的原型对象
    function F () {}
    F.prototype = proto
    return new F()
}

/**
* 改良版本
* @param {Object} proto
*
*/
Object.prototype.createX = function (proto) {
    const obj = {}
    // 一步到位,把一个空对象的原型链(__proto__)指向指定原型对象即可
    Object.setPrototypeOf(obj, proto)
    return obj
} 

我们可以看到,Object.create(proto)做的事情转换成其他方法实现很简单,就是创建一个空对象,再把这个对象的原型链属性(Object.setPrototype(obj, proto))指向指定的原型对象proto就可以了(不要采用直接赋值__proto__属性的方式,因为每个浏览器的实现不尽相同,而且在规范中也没有明确该属性名)。

四、Function的原型方法:call、apply和bind

作为最经典的手写“劳模”们,callapplybind已经被手写了无数遍。也许本文中手写的版本是无数个前辈们写过的某个版本,但是有一点不同的是,本文会告诉你为什么要这样写,让你搞懂了再写。

在《你不知道的Javascript上卷》第二部分的第1章和第2章,用了2章斤30页的篇幅中详细地介绍了this的内容,已经充分说明了this的重要性和应用场景的复杂性。

而我们要实现的callapplybind最为人所知的功能就是使用指定的thisArg去调用函数,使得函数可以使用我们指定的thisArg作为它运行时的上下文。

《你不知道的Javascript》总结了四条规则来判断一个运行中函数的this到底是绑定到哪里:

  1. new 调用?绑定到新创建的对象。
  2. call 或者 apply (或者 bind )调用?绑定到指定的对象。
  3. 由上下文对象调用?绑定到那个上下文对象。
  4. 默认:在严格模式下绑定到 undefined ,否则绑定到全局对象。

更具体一点,可以描述为:

  1. 函数是否在 new 中调用( new 绑定)?如果是的话 this 绑定的是新创建的对象:
var bar = new foo() 
  1. 函数是否通过 callapply (显式绑定)或者硬绑定(bind)调用?如果是的话, this 绑定的是指定的对象:
var bar = foo.call(obj2) 
  1. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话, this 绑定的是那个上下文对象:
var bar = obj1.foo() 
  1. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined ,否则绑定到全局对象:
var bar = foo() 

就是这样。对于正常的函数调用来说,理解了这些知识你就可以明白 this 的绑定原理了。

至此,你已经搞明白了this的全部绑定规则,而我们要去手写实现的callapplybind只是其中的一条规则(第2条),因此,我们可以在另外3条规则的基础上很容易地组织代码实现。

4.1 call和apply

实现callapply的通常做法是使用“隐式绑定”的规则,只需要绑定thisArg对象到指定的对象就好了,即:使得函数可以在指定的上下文对象中调用:

const cOntext= {
    name: 'ZhangSan'
}
function sayName () {
    console.log(this.name)
}
context.sayName = sayName
context.sayName() // ZhangSan 

这样,我们就完成了“隐式绑定”。落实到具体的代码实现上:

/**
* @param {context} Object 
* @param {arg1, arg2, ...} 指定的参数列表
*/
Function.prototype.call = function (context, ...args) {
    // 指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装
    if (cOntext=== null || cOntext=== undefined) {
        cOntext= window
    } else if (typeof context !== 'object') {
        cOntext= new context.constructor(context)
    } else {
        cOntext= context
    }
    const func = this
    const fn = Symbol('fn')
    context[fn] = func
    const result = context[fn](...args)
    delete context[fn]
    return result
}

/**
* @param {context}
* @param {args} Array 参数数组
*/
Function.prototype.apply = function (context, args) {
    // 和call一样的原理
    if (cOntext=== null || cOntext=== undefined) {
        cOntext= window
    } else if (typeof context !== 'object') {
        cOntext= new context.constructor(context)
    } else {
        cOntext= context
    }
    const fn = Symbol('fn')
    const func = this
    context[fn] = func
    const result = context[fn](...args)
    delete context[fn]
    return result
} 

细看下来,大家都那么聪明,肯定一眼就看到了它们的精髓所在:

const fn = Symbol('fn')
const func = this
context[fn] = func 

在这里,我们使用Symbol('fn')作为上下文对象的键,对应的值指向我们想要绑定上下文的函数this(因为我们的方法是声明在Function.prototype上的),而使用Symbol(fn)作为键名是为了避免和上下文对象的其他键名冲突,从而导致覆盖了原有的属性键值对。

4.2 bind

在《你不知道的Javascript》中,手动实现了一个简单版本的bind函数,它称之为“硬绑定”:

function bind(fn, obj) {
    return function() {
        return fn.apply( obj, arguments );
    };
} 

硬绑定的典型应用场景就是创建一个包裹函数,传入所有的参数并返回接收到的所有值。

由于硬绑定是一种非常常用的模式,所以在 ES5 中提供了内置的方法 Function.prototype.bind ,它的用法如下:

function foo(something) {
    console.log( this.a, something )
    return this.a + something;
}

var obj = {
    a:2
}

var bar = foo.bind( obj )

var b = bar( 3 ); // 2 3

console.log( b ); // 5 

bind(..) 会返回一个硬编码的新函数,它会把参数设置为 this 的上下文并调用原始函数。

MDN是这样描述bind方法的:bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

因此,我们可以在此基础上实现我们的bind方法:

/**
* @param {context} Object 指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装
*
* @param {arg1, arg2, ...} 指定的参数列表
*
* 如果 bind 函数的参数列表为空,或者thisArg是null或undefined,执行作用域的 this 将被视为新函数的 thisArg
*/
Function.prototype.bind = function (context, ...args) {
    if (typeof this !== 'function') {
        throw new TypeError('必须使用函数调用此方法');
    }
    const _self = this

    // fNOP存在的意义:
    //  1. 判断返回的fBound是否被new调用了,如果是被new调用了,那么fNOP.prototype自然是fBound()中this的原型
    //  2. 使用包装函数(_self)的原型对象覆盖自身的原型对象,然后使用new操作符构造出一个实例对象作为fBound的原型对象,从而实现继承包装函数的原型对象
    const fNOP = function () {}

    const fBound = function (...args2) {

        // fNOP.prototype.isPrototypeOf(this) 为true说明当前结果是被使用new操作符调用的,则忽略context
        return _self.apply(fNOP.prototype.isPrototypeOf(this) && context ? this : context, [...args, ...args2])
    }

    // 绑定原型对象
    fNOP.prototype = this.prototype
    fBound.prototype = new fNOP()
    return fBound
} 

具体的实现细节都标注了对应的注释,涉及到的原理都有在上面的内容中讲到,也算是一个总结和回顾吧。

五、函数柯里化

维基百科:柯里化,英语:Currying,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术 。

看这个解释有一点抽象,我们就拿被做了无数次示例的add函数,来做一个简单的实现:

// 普通的add函数
function add(x, y) {
    return x + y
}

// Currying后
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}

add(1, 2)           // 3
curryingAdd(1)(2)   // 3 

实际上就是把add函数的xy两个参数变成了先用一个函数接收x然后返回一个函数去处理y参数。现在思路应该就比较清晰了,就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

函数柯里化在一定场景下,有很多好处,如:参数复用、提前确认和延迟运行等,具体内容可以拜读下这篇文章,个人觉得受益匪浅。

最简单的实现函数柯里化的方式就是使用Function.prototype.bind,即:

function curry(fn, ...args) {
    return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
} 

如果用ES5代码实现的话,会比较麻烦些,但是核心思想是不变的,就是在传递的参数满足调用函数之前始终返回一个需要传参剩余参数的函数:

// 函数柯里化指的是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。
function curry(fn, args) {
    args = args || []

    // 获取函数需要的参数长度
    let length = fn.length
    return function() {
        let subArgs = args.slice(0)

        // 拼接得到现有的所有参数
        for (let i = 0; i = length) {
            // 如果满足,执行函数
            return fn.apply(this, subArgs)
        } else {
            // 如果不满足,递归返回科里化的函数,等待参数的传入
            return curry.call(this, fn, subArgs)
        }
    };
} 

六、文末总结

在本文中,我们熟悉了new的底层执行原理、instanceof和原型链直接的密切关系、Object.create()是如何实现的原型链指定以及Javascript中最复杂最难搞定的this的绑定问题。

当然,在《你不知道的Javascript》中还有很多精妙的见解和知识内容,如"Javascript是需要先编译再执行的"、"面相对象编程只是Javascript中的一种设计模式"等等独到的见解和观点。墙裂推荐大家多读几遍,每一遍都会有不同的收获。

相关免费学习推荐:Javascript(视频)

以上就是你所不知道的Javascript的详细内容,更多请关注 第一PHP社区 其它相关文章!


推荐阅读
  • 本文介绍了SIP(Session Initiation Protocol,会话发起协议)的基本概念、功能、消息格式及其实现机制。SIP是一种在IP网络上用于建立、管理和终止多媒体通信会话的应用层协议。 ... [详细]
  • 在日常生活中,支付宝已成为不可或缺的支付工具之一。本文将详细介绍如何通过支付宝实现免费提现,帮助用户更好地管理个人财务,避免不必要的手续费支出。 ... [详细]
  • 解决Win10系统显示模糊问题的方法
    Windows 10作为一款深受用户喜爱的操作系统,提供了丰富的个性化设置选项,让每位用户都能根据个人偏好定制系统界面。然而,在进行个性化设置的过程中,有时会出现显示模糊的情况,影响用户体验。本文将详细介绍如何有效解决Win10系统显示模糊的问题。 ... [详细]
  • 拨打电话时遇到‘正在通话’或无人接听的情况解析及解决方法
    当您尝试联系他人时,可能会遇到电话提示‘正在通话’或是虽然接通但无人应答的情况。本文将详细解释可能的原因,并提供相应的解决策略。 ... [详细]
  • 本文详细介绍了在Windows系统中如何配置Nginx以实现高效的缓存加速功能,包括关键的配置文件设置和示例代码。 ... [详细]
  • 如何在PyCharm中配置Python脚本的默认模板
    本文介绍如何在PyCharm中设置Python脚本的默认模板,以便每次创建新的.py文件时自动填充预设内容,提高开发效率。 ... [详细]
  • 当需要确保对象的状态在创建后不可更改时,使用记录(Record)类型是一个理想的选择。本文探讨了如何通过记录类型实现对象的不可变性,并提供了一个简单的示例来说明其用法。 ... [详细]
  • 本文详细介绍了FLV播放器的构建过程,包括如何解析FLV标签并将这些标签传递给解码器,以及如何在Qt环境中注册共享指针的信号和槽机制。 ... [详细]
  • 团支部的概念及其职能
    本文详细介绍了团支部的基本概念、组织结构以及其在共青团体系中的重要职能。 ... [详细]
  • 在开发一个网页音乐播放器时遇到问题,需要从不同源读取MP3文件的ID3标签信息,包括流派、歌手和歌曲名称等。尝试使用PHP未果后转而考虑使用JavaScript进行跨域读取,但不清楚具体配置方法,寻求技术指导。 ... [详细]
  • 探索《冯诺依曼传》:天才与时代的交响
    本文深入探讨了《冯诺依曼传》,通过分析这位20世纪杰出科学家的生平,揭示其对现代科技及理论科学的深远影响。 ... [详细]
  • 本文详细介绍了JQuery Mobile框架中特有的事件和方法,帮助开发者更好地理解和应用这些特性,提升移动Web开发的效率。 ... [详细]
  • CRZ.im:一款极简的网址缩短服务及其安装指南
    本文介绍了一款名为CRZ.im的极简网址缩短服务,该服务采用PHP和SQLite开发,体积小巧,约10KB。本文还提供了详细的安装步骤,包括环境配置、域名解析及Nginx伪静态设置。 ... [详细]
  • 我的读书清单(持续更新)201705311.《一千零一夜》2006(四五年级)2.《中华上下五千年》2008(初一)3.《鲁滨孙漂流记》2008(初二)4.《钢铁是怎样炼成的》20 ... [详细]
  • 随着市场需求的增长,各行业对兼职人员的需求日益增加。无论是本地生活服务还是专业技能提供,兼职APP成为了连接供需双方的重要桥梁,有效地提升了信息透明度和服务效率。 ... [详细]
author-avatar
lhpa
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有