あなたの知らないJavaScript

coldplay.xixi
リリース: 2020-11-17 16:30:35
転載
2472 人が閲覧しました

JavaScript 列では、知っておくべき操作をいくつか紹介します。

あなたの知らないJavaScript

他の「サークル」の学生とは異なり、フロントエンドサークルの学生は「手書きxxxメソッド」に非常に熱心で、基本的に毎日ナゲッツで過ごすことができます。同様の記事を参照してください。ただし、多くの記事 (すべてを代表しているわけではなく、攻撃を意図したものではありません) は、ほとんどが真実を鵜呑みにし、以前のものをコピーしているため、精査や研究に耐えることができず、JavaScript を始めたばかりの新入生を簡単に誤解させる可能性があります。

これを考慮して、この記事は、『あなたが知らない JavaScript』 (リトル イエロー ブック) のいくつかの典型的な知識ポイントに基づいて、いくつかの古典的で使用頻度の高い「手書き」の方法を組み合わせて説明します。原則と原則を 1 つずつ説明し、実装を組み合わせて、コードを手で書く前に学生が協力して原理を理解します。

1. 演算子 new

説明する前に、まず JavaScript の関数とオブジェクトに関するよくある誤解を明確にする必要があります。 , 「コンストラクター」はクラス内の特別なメソッドです。

new

を使用してクラスを初期化すると、クラス内のコンストラクターが呼び出されます。通常の形式は次のようになります: <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">something = new MyClass(..);复制代码</pre><div class="contentsignin">ログイン後にコピー</div></div>JavaScript にも

new

演算子があり、使用方法はクラス指向言語と同じように見えます。 ##new の仕組みもそれらの言語と同じです。ただし、JavaScript の new の仕組みは、実際にはクラス指向言語の仕組みとはまったく異なります。 まず、JavaScript の「コンストラクター」を再定義しましょう。 JavaScript では、コンストラクターは、new

演算子を使用するときに呼び出される単なる関数です。これらはクラスに属しておらず、クラスをインスタンス化することもありません。実際、これらは特別な関数タイプであるとさえ言えず、

new 演算子によって呼び出される単なる通常の関数です。 実際には、いわゆる「コンストラクター」はなく、関数には「コンストラクター呼び出し」があるだけです。 new

を使用して関数を呼び出すか、コンストラクター呼び出しが発生すると、次の操作が自動的に実行されます:

新しいオブジェクトを作成 (または構築) します。

この新しいオブジェクトは [[ プロトタイプ]] 接続で実行されます;
  1. この新しいオブジェクトは関数呼び出しの this にバインドされます;
  2. 関数が他のオブジェクトを返さない場合、次に、新しい式 式内の関数呼び出しにより、この新しいオブジェクトが自動的に返されます。
  3. したがって、理論的な
  4. new
  5. を記述したい場合は、上記の手順に厳密に従い、それをコードに実装する必要があります。
/**
* @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() メソッドも手書きして、その方法を理解します。 2. 演算子instanceof

長い間、JavaScriptには、
new

instanceof ##など、クラスに似たいくつかの構文要素しかありませんでした。 # ただし、

class キーワードなど、後の ES6 ではいくつかの新しい要素が追加されました。 class を考慮しないと、new

instanceof の関係は「曖昧」になります。 new および instanceof 演算子の主な目的は、「オブジェクト指向プログラミング」に近づくことです。 したがって、new を理解した以上、instanceof

を理解しない理由はありません。 MDN の

instanceof の説明を引用します: 「instanceof 演算子は、コンストラクターの prototype 属性がインスタンス オブジェクトのプロトタイプ チェーンに現れるかどうかを検出するために使用されます。 。」 これを見た後、instanceof の実装では、プロトタイプ チェーンと prototype

の理解をテストする必要があることが基本的にわかりました。 JavaScript のプロトタイプとプロトタイプ チェーンに関する内容は、わかりやすく説明するには大量の内容が必要ですが、インターネット上には優れたまとめブログ記事もいくつかあります。その 1 つは、JS のプロトタイプ、__proto__、およびコンストラクター (図) を徹底的に理解するのに役立ちます。 . それらの関係性や繋がりを明確に梳いてまとめた稀有な秀逸な論文。

「あなたが知らない JavaScript」の第 2 部 - 第 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)
    }
}复制代码
ログイン後にコピー
3. Object.create()

Object.create()

メソッドは新しいオブジェクトを作成し、既存のオブジェクトを使用して新しく作成されたオブジェクトを提供します

__プロト__

在《你不知道的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 < arguments.length; i++) {
        subArgs.push(arguments[i])
        }

        // 判断参数的长度是否已经满足函数所需参数的长度
        if (subArgs.length >= 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 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:juejin.im
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート