普通视图

发现新文章,点击刷新页面。
昨天 — 2025年5月18日首页

面试之道——手写call、apply和bind

作者 哆啦美玲
2025年5月18日 18:30

嗨嗨嗨~这里是哆啦美玲分享的知识点,一起来学呀!

这次的文章基于我之前写的this的显式绑定的文章,有不懂的可以倒回去看看哦——搞懂this,如此简单 - 掘金

image.png

callapplybind 都是 JavaScript 中函数的调用方法,它们的作用是改变函数的上下文 (this) 和传递参数,但它们之间有一些不同点。

首先我们看下面这段代码:

let obj = {
    a: 1
}

function foo(x, y) {
    console.log(this.a, x + y);
    return 'hello'
}
console.log(foo(1, 2)); // undefined 3 hello

我们声明了一个对象obj和函数foo,在独立调用foo时,this指向的是全局,所以this.a会返回undefiend。那我们如何实现this指向obj呢?

一、call

1. call的特点

  • call 方法立即调用一个函数,并且可以指定 this 的值,同时传入参数。
  • 参数是按顺序传递的,多个参数使用逗号分隔。
const res = foo.call(obj, 3, 4) // call的this指向foo,foo的this指向obj
console.log(res); // 1 7 hello

2. 手写myCall(context, ...args)

在手写myCall之前我们需要分析:myCall写哪里? image.png 如图的代码结果,foo既是函数也是对象,但是使用对象obj调用call方法会报错:call is not a function,所以myCall方法写在构造函数Function的原型(Function.prototype)。

因为函数也是对象,所以foo.call()会导致call的this指向foo;call的执行会让obj调用foo,让foo的this指向obj;

foo在声明前需要多少参数call并不清楚,所以call可以使用...args的形式接收剩余参数;另外,我们写的foo是有返回值的,在call的调用后会返回foo的结果,所以我们手写call的时候也要写返回值。

我们再看,如果call传入的参数中第一位不是对象,会得到什么? image.png 从图上代码的输出结果可以看出来:call会把传入的参数除去第一位后按顺序交给foo,且第一位必须是对象才能改变this的指向。

根据分析,手写代码如下:

// 手写call
Function.prototype.myCall = function (context, ...args) { // foo.__proto__ == Function.prototype
    if (typeof (this) !== 'function') { // 判断this必须是函数
        throw new TypeError('Error')
    }
    context = context || window

    const key = Symbol('fn') // 唯一的key
    context[key] = this // this = foo
    const res = context[key](...args) // 隐式绑定 this = context
    delete context[key]
    return res
}
console.log(foo.myCall(obj, 2, 3)); // 1 5 hello

代码第6行 context = context || window 的意思是:如果 context 变量已经有值(即不是 null 或 undefined),那么就使用 context 的值;如果 context 没有值(即为 null 或 undefined),就使用 window 作为默认值。

代码第8-9行是给foo函数创建一个唯一的key值,确保不会修改掉函数内部原本的属性值。

最后为了不修改原对象,要记得把新增的属性删除!

二、apply

1. apply的特点

  • apply方法也立即调用一个函数,并指定 this 的值,同时传入参数。
  • 参数传递方式是将一个数组或类数组对象作为参数列表传入。
const res1 = foo.apply(obj, [3, 4]) // apply的this指向foo, foo的this指向obj
console.log(res1); // 1 7 hello

2. 手写myApply

apply与call的用法是一样的,唯一不同的就是接收的参数不一样,所以只需要修改一点点就可以了,代码如下:

// 手写apply
Function.prototype.myApply = function (context, args) { // foo.__proto__ == Function.prototype
    if (typeof (this) !== 'function') { // 判断this必须是函数
        throw new TypeError('Error')
    }
    context = context || window

    const key = Symbol('fn')
    context[key] = this // this = foo
    const res = context[key](...args) // 隐式绑定 this = context
    delete context[key]
    return res
}

console.log(foo.myApply(obj, [2, 4])); // 1 6 hello

三、bind

1. bind的特点

  • bind方法并不立即调用函数,而是返回一个新的函数,新的函数会绑定指定的 this 和参数。
  • 这个返回的新函数可以在之后的某个时刻被调用,且新函数也可以接收零散参数。
  • 当新函数被 new 调用时, 返回的是调用 bind 的那个函数的实例对象
const fn = foo.bind(obj, 4)
const res2 = fn(4)
console.log(res2); // 1 8 hello

const f = new fn(4)
console.log(f); // undefined 8 foo {}

从代码中可以看出:bind函数调用时,foo函数在接收参数时会先在bind传入的参数里面按顺序找,如果不够再去找bind返回的新函数f传入的参数找。

另外,在代码的5-6行我们会发现,在new fn()时,本来应该返回一个fn的实例对象fn{},但实际返回的却是foo的实例对象foo{},并且foo中的this指向了全局,所以在new的过程会导致this指向全局,fn()执行返回foo的实例对象。

2. 手写myBind

bind与前面两个方法很不一样,第一需要注意的点就是调用bind后会返回一个新的函数体。

接下来我们看看:如果bind传入的第一个不是参数,新函数会是什么? image.png

从图中的结果可知:foo.bind(123)返回的是一个foo的实例对象,所以this指向的是全局。

另外,在前面我们已经说了如果我们new foo.bind(obj)得到的新函数,也会得到一个foo的实例对象。所以bind返回的函数体不能是箭头函数,因为箭头函数里面没有this,不能被new。

这就需要我们区分new fn()fn():因为new会使函数的this直接指向得到的实例对象,且让实例对象的隐式原型等于构造函数的显式原型,所以我们采用判断this.__proto__ === F.prototype来判断是否被new。

代码如下:

Function.prototype.myBind = function (context, ...args) {
    if (typeof (this) !== 'function') { // 判断this必须是函数
        throw new TypeError('Error')
    }
    context = context || window
    const self = this // 存储this的值 foo

    return function F(...args2) { 
        if(this.__proto__ === F.prototype){ // 被 new 直接返回 foo 实例,所以这里不能是箭头函数
            return new self(...args, ...args2) // 返回foo 的实例对象
        }else{
            return self.apply(context, [...args, ...args2])  // foo指向obj,foo执行且返回值接收并返回
        }
    }
}

const fo = foo.myBind(obj, 1, 2)
console.log(fo(),'//////'); // 1 3 hello //////

const fun = new fo()
console.log( 'new fo() 得到的结果:', fun); // undefined 3  new fo() 得到的结果:foo {}

好啦,本次知识点分享完毕,家人们下次见!!!

喜欢这次的文章就麻烦点个赞赞啦~谢谢大家!

image.png

昨天以前首页

JavaScript中的类型判断方法你知道几种?

作者 哆啦美玲
2025年5月17日 08:09

hello~好久不见,这里是哆啦美玲哈哈!最近觉得记住知识点最好的方法,还是回顾的时候写文章分享给大家最有用,虽然我写的一般,大家将就看吧。今天就是分享一个js知识点——如何判断数据类型?

image.png

数据类型

js中的数据类型分两种:基本类型和引用类型对象。

基本类型(原始类型) 包括:number、string、boolean、null、undefined、symbol、bigInt

引用类型包括 :Object、array、function、Data

类型判断的方法

1. typeof 判断原始类型

typeof 可以准确的判断除了null 之外的所有原始类型,不能准确判断引用类型,除了function

那typeof是怎么使用的呢?我们直接看下面的代码:

// 判断基本类型
console.log(typeof 'hello'); //string
console.log(typeof 123); // number
console.log(typeof true); // boolean
console.log(typeof null); // object
console.log(typeof undefined); // undefined
console.log(typeof Symbol(1)); // symbol
console.log(typeof 123n); // bigint

// 判断引用类型
console.log(typeof {}); // object
console.log(typeof []); // object
console.log(typeof new Date()); // object
console.log(typeof function foo(){}); // function

由此,我们可以发现,直接使用 typeof 关键词去判断基本类型时,只有null一种类型会被误判成为object,其他基本数据类型都能够成功判断。

其实在之前的文章中解释过: 了解JavaScript的底层——内存机制 - 掘金

这里我们再解释一次,因为JS在判断类型时,会把变量的值转换成二进制进行判断;在计算机中所有的引用类型的二进制前三位是0,而null转换成二进制全部都是0,所以被误判成是对象。此外,最特殊的函数使用typeof是可以准确的判断出是function。

2. instanceof 判断引用类型
2.1 原理

instanceof 关键字是通过原型链来判断类型相等,只能判断引用类型(原始类型没有隐式原型)

我们直接看代码举例:

console.log({} instanceof Object);  // instanceof关键字(隶属于) 输出true
console.log([] instanceof Array);  // true
console.log(new Date() instanceof Date);  // true
console.log(function(){} instanceof Function);  // true

console.log([] instanceof Object);  // true
console.log(new Date() instanceof Object);  // true
console.log(function(){} instanceof Object);  // true

// console.log(null instanceof null); // 报错,右边必须是对象 
console.log("hello" instanceof String);  // false
console.log(123 instanceof Number);  // false
console.log(true instanceof Boolean);  // false

从上面的代码结果来看,数组、函数等对象,不仅能够判断它们是否隶属于他们本身的构造函数,还能判断是否是一个对象。所以instanceof关键字是判断隶属于的关系,即判断某个变量是否隶属于某种类型,返回true或者false。而在第10行中我们如果执行它,会得到报错提示:Right-hand side of 'instanceof' is not an object,这告诉我们instanceof的右边必须放一个对象才能进行判断。

但是它到底是怎么判断的呢?我们看下面一段代码:

function Car(){
    this.run = 'running'
}

Bus.prototype = new Car()

function Bus(){
    this.name = 'BYD'
}

let bus = new Bus();

console.log(bus instanceof Bus); // true  bus.__proto__ == Bus.prototype
console.log(bus instanceof Car); // true  bus.__proto__.__proto__ == Car.prototype
console.log(bus instanceof Object); // true bus.__proto__.__proto__.__proto__ == Object.prototype

我们自己创造一个构造函数Bus,并且基于Bus构建一个实例对象bus,所以13行代码返回true很好理解。然后我们又创造了一个Car的构造函数,并且让Bus的对象原型是Car的实例对象后,我们打印14行的结果是true,为什么呢?

其实,根据代码的结果,我们应该可以猜到instanceof可能是根据原型链来进行判断,我们直接来看bus、Bus和Car之间有什么关联:

首先我们回顾new的原理:将构造函数的显示原型(Object.prototype) 赋值给 实例对象的对象原型(即隐式原型obj.__ proto__)。(不懂的可以去看这篇文章:搞懂this,如此简单 - 掘金

所以11行代码构造实例对象bus时,会实现 bus.__ proto__ == Bus.prototype。

而第五行代码Bus.prototype = new Car(),其中Bus.prototype也是一个对象,它也存在对象原型(隐式原型),所以我们人为的将构造函数Car的显示原型赋值给它的对象原型,即 Bus.prototype.__ proto__ == Car.prototype。所以我们就可以将Bus.prototype替换掉,得到 Bus.prototype._ proto_ == bus.__ proto__._ proto_ == Car.prototype

第15行代码也同理,顺着原型链进行追溯,相信你们也可以推理得到 bus.__ proto__.__ proto__.__ proto__ == Object.prototype。所以,instanceof就是根据原型链来判断数据是否为引用类型。

2.2 手写myInstanceof方法

前面我们已经搞清楚了instanceof的判断原理,所以我们也可以手戳一个instanceof方法,实现同理。这里我直接展示我使用的两种方法:循环和递归,代码如下:

// 方法一:while循环
function my_instanceof(L, R) {
    while (L !== null) {
        L = L.__proto__
        if (L === R.prototype) {
            return true
        }
    }
    return false
}
console.log(my_instanceof([], Object));

// 方法二:递归
function myInstanceof(L, R) {
    if (L !== null) {
        L = L.__proto__
        if (L !== R.prototype) {
            return myInstanceof(L, R); 
            //每次递归的判断结果 (true 或 false) 都需要返回给上一层应该是直接返回递归的结果,
            // 确保每层递归都能够返回正确的判断。
        }
        return true
    }
    return false

}
console.log(myInstanceof([],Array)); 
2.3 包装类
console.log(new String('hello') instanceof String); // true
console.log("hello" instanceof String); // false

我们总说let str = 'hello' 和 let str = new String('hello') 是一样的,但是为什么上面代码会出现两个结果呢?这就是我们要聊的包装类的问题。

在JavaScript中,并没有像Java中那样明确的“包装类”概念。JavaScript是一种动态类型语言,变量的类型是可以在运行时决定的,因此并不需要使用包装类来将原始数据类型(基本类型)封装成对象。然而,JavaScript仍然有一些类似的概念,尤其是与基本数据类型(例如 string、number、boolean)相关的对象封装

  1. 显式创建包装对象,即直接用对应的构造函数创建变量,会将原始数据类型包装为对象,并允许访问它们的属性和方法。例如:构建字符串对象 let str = new String('abc')。
  2. 基本数据类型在V8执行下会自动被封装为对象类型。当我们访问字符串、数字、布尔值等原始类型的属性或方法时,JavaScript会自动将它们包装成相应的包装类对象。即在V8的眼里let a = 1会被执行成 let a = new Number(1)
  3. 原始类型不能拥有属性和方法,属性和方法只能是引用类型的。
  4. 访问对象身上不存在的属性不会报错,会是undefined

方便理解,我举个例子:

let num = 123  // let num = new Number(123)
num.a = 1  // {a:1}
// delete num.a
console.log(num.a); // undefined
console.log(num); // 123

代码执行的逻辑如下:

  1. 第一行代码在V8眼里会执行成let num = new Number(123),所以第二行代码能够往num上添加a属性。
  2. 我们需要的num是原始类型,不是包装类,V8严格按照原始类型不能拥有属性和方法这一原理,会将num身上的a属性移除,即delete num.a
  3. 访问Number对象 num身上不存在的属性是不会报错的,会返回undefined。
  4. 读取值的时候会执行:读取[[PrimitiveValue]]的值。如下图,num内部其实有一个内置属性[[PrimitiveValue]],是只有V8能够访问的用来存取原始类型的值的属性。

f52ac2729f2399424b0e7aac7774797.png

上面例子是没有直接显示创建包装类的情况,如果是显示创建的情况如下: image.png

我们会发现,显示创建包装类的情况下,V8是直接当做一个对象进行处理的,所以可以往其身上添加属性和方法。

3. Object.prototype.toString().call(x)

Object.prototype.toString().call(x)是借助Object原型上的toString方法在执行过程中会读取 X 的内部属性[[Class]]这一机制来进行数据的类型判断。如图: f14dd0e23455a33df72e10eb9abc0a2.png

在官方文档中关于Object.prototype.toString()触发时会执行以下步骤描述如下::

  1. 如果 this 值为 undefined,返回 “[object Undefined]”。
  2. 如果 this 值为 null,返回 “[object Null]”。
  3. 设 O 为调用 ToObject(C打造的V8用的方法) 的结果,并将 this 值作为参数,即V8会执行ToObject(this)
  4. class 为 O 的 [[Class]] 内部属性的值,即class等于O的数据类型。
  5. 返回 String 值,该值是将三个字符串拼接—— "[object " 、 class 和 "]"

根据上面的步骤,我总结为:Object.prototype.toString()会读取this的值身上的内置属性[[Class]] ——对象的类型,以一个很特殊的状态返回 “[object ”、class 和 “]”

所以这个方法关键在于 this 指向谁,这就要使用call() 将this显示绑定在需要判断的对象上,就能够返回能够显示对象类型的字符串。(有不熟悉call的可以看这篇:搞懂this,如此简单 - 掘金

测试代码如下:

let a = 1 
let b = {}
console.log(Object.prototype.toString.call(a)); // [object Number] 
console.log(Object.prototype.toString.call(b)); // [object Object]
4. Array.isArray() 判断数组

Array.isArray()是专门用来判断一个对象是不是数组的方法,代码如下:

let arr = []
// 判断一个对象是不是数组
console.log(Array.isArray({}));

好了,我的分享到这里就结束啦~喜欢我的分享记得给我点个赞喔!

ღ( ´・ᴗ・ `)比心,我们下次再见!!

image.png

❌
❌