解密JavaScript面向对象(一):从新手到高手,手写call/bind实战

为什么先学面向对象?

很多新手朋友会问:现在React、Vue这么火,为什么还要学这些"过时"的基础?
答案很简单:框架会变,但编程思想永不过时。面向对象是理解现代前端框架的基石,React的组件、Vue的实例,本质上都是面向对象思想的体现。
了解并理解JavaScript的重要特性,才能更好地理解第三方库源码,编写更优雅的兼容性代码,在复杂场景下游刃有余,在面试中展现深度技术理解。

一、面向对象:用生活的思维解读代码

从现实世界到代码世界:
想象你要在程序中管理一个图书馆:
传统做法(面向过程):

// 一堆零散的数据和函数
let book1Title = "JavaScript高级程序设计";
let book1Author = "张三";
let book1Borrowed = true;

let book2Title = "CSS世界";
let book2Author = "李四"; 
let book2Borrowed = false;

function borrowBook(bookTitle) {
    // 复杂的逻辑...
}

面向对象做法:

class Book {
    // 构造函数,构造书本的三种要素title、author、是否被借阅
    constructor(title, author) {
        this.title = title;
        this.author = author;
        this.isBorrowed = false;
    }
    // 执行借出操作
    borrow() {
        if (this.isBorrowed) {
            console.log(`${this.title}已被借出`);
            return;
        }
        this.isBorrowed = true;
        console.log(`成功借阅${this.title}`);
    }
    // 执行还书操作 
    returnBook() {
        this.isBorrowed = false;
        console.log(`已归还${this.title}`);
    }
}

// 定义书本 
const book1 = new Book("JavaScript高级程序设计", "张三");
const book2 = new Book("CSS世界", "李四");
// 执行借出操作 
book1.borrow();  // 成功借阅JavaScript高级程序设计

看到区别了吗?面向对象让代码更像现实世界,更易理解、更易维护!

二、参数传递:JS中的"值传递"陷阱

这是个面试必问题,也是实际开发中常见的坑。

基本类型:真正的值传递

let a = 10;
function change(num) {
    num = 20;  // 修改的是副本
    console.log("函数内:", num); // 20
}
change(a);
console.log("函数外:", a); // 10 (原值不变)

对象类型:引用地址的传递

let obj = { value: 10 };
function change(objParam) {
    objParam.value = 20;  // 通过地址修改原对象
    console.log("函数内:", objParam.value); // 20
}
change(obj);
console.log("函数外:", obj.value); // 20 (原对象被修改了!)

关键理解:传递对象时,传递的是"地址的拷贝",两个变量指向同一个对象!

总结:
基本类型的值存储于栈内存中,传递的就是当前值,修改不会影响原有变量的值;
对象类型的值是真实值所在的堆内存中的索引地址,传递的就是当前索引地址,值改变时会改变对内存中的实际值。

三、this指向:面向对象的核心难题

很多新手被this搞晕,其实记住一句话:this指向调用它的对象

const person = {
    name: "小明",
    introduce: function() {
        console.log(`我是${this.name}`);
    }
};
person.introduce(); // 我是小明(this指向person)
const intro = person.introduce;
intro(); // 我是undefined(this指向window/global)

四、手写call/apply/bind:深入理解this绑定与指向

先看原生用法

const person1 = { name: "小王" };
const person2 = { name: "小张" };
const person3 = { name: "小安" };
function introduce(age, job) {
    console.log(`我是${this.name}, 今年${age}岁, 是一名${job}`);
}
introduce.call(person1, 25, "前端工程师");    // 我是小王...
introduce.apply(person2, [28, "后端工程师"]); // 我是小张...
const intro = introduce.bind(person3); //返回一个函数
intro(27"算法工程师"); // 我是小安...

call:在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。
apply:雷同call方法,区别在于参数为参数数组。
bind:创建一个新函数。当这个新函数被调用时,bind的第一个参数将作为它运行时的 this,之后的参数将会在传递的实参前传入作为它的参数。

4.1 手写call / apply方法

基本思路:1. 将函数设为对象的属性;2. 执行该函数;3.删除该函数属性

// 结合上面的例子代码如下
// person1.fn = introduce
// person1.fn()
// delete person1.fn
// 第一版如下 结合原生用法来理解 
Function.prototype.call01 = function(context){
    // introduce.call(...),此时this指向调用它的对象:introduce
    context.fn = this;
    //执行introduce方法
    context.fn();
    //删除定义函数属性fn
    delete context.fn;
}

增加考虑项1:call指定的this参数为null情况

// 第二版如下  结合原生用法来理解 
Function.prototype.call02 = function(context){
    // 判断是否是undefined和null
    if (typeof context === 'undefined' || context === null) {
      context = window
    }
    // introduce.call(...),此时this指向调用它的对象:introduce
    context.fn = this;
    //执行introduce方法
    context.fn();
    //删除定义函数属性fn
    delete context.fn;
}

增加考虑项2:call除了可以指定this,还可以携带参数

// 第三版如下
Function.prototype.call03 = function(context){
    // 判断是否是undefined和null
    if (typeof context === 'undefined' || context === null) {
      context = window
    }
    // introduce.call(...),此时this指向调用它的对象:introduce
    context.fn = this;
    //获取参数
    let arg = [...arguments].slice(1)
    //执行introduce方法
    context.fn(...arg);
    //删除定义函数属性fn
    delete context.fn;
}

增加考虑项3:当函数执行体有返回情况

// 最终版
Function.prototype.MyCall = function(context){
    // 判断是否是undefined和null
    if (typeof context === 'undefined' || context === null) {
      context = window
    }
    // introduce.call(...),此时this指向调用它的对象:introduce
    // 优化fn的唯一性
    let fnSy = Symbol()
    context[fnSy] = this;
    //获取参数
    let arg = [...arguments].slice(1)
    //执行introduce方法 - 增加返回结果的接收 
    let fnReturn = context.fn(...arg);
    //删除定义函数属性fn
    delete context[fnSy];
    // 返回结果值
    return fnReturn
}

同理可见apply的实现如下

Function.prototype.MyApply = function(context, arr){
    // 判断是否是undefined和null
    if (typeof context === 'undefined' || context === null) {
      context = window
    }
    // introduce.call(...),此时this指向调用它的对象:introduce
    // 优化fn的唯一性
    let fnSy = Symbol()
    context[fnSy] = this;
    //执行introduce方法 - 增加返回结果的接收 
    let fnReturn = context.fn(...arr);
    //删除定义函数属性fn
    delete context[fnSy];
    // 返回结果值
    return fnReturn
}

4.2 手写bind方法

基本思路:1. 处理预制参数和合并参数;2. 改变 this 指向;3.维护原型关系;4. 返回函数体

// 结合上面的例子代码如下
Function.prototype.bind01 = function(context){
    // 保存绑定时的this指向 introduce.bind(person)
    let self = this;
    // 获取预制参数,若introduce.bind(person, {***}),***则为预制参数
    // 获取arguments除第一个参数context后的其他参数 
    let args = Array.prototype.slice.call(arguments, 1);
    let fBind = function(){
        // 获取bind后函数执行时的参数,便与跟预制参数合并
        let bindArgs = Array.prototype.slice.call(arguments);
        // 改变this指向,传入合并参数 
        return self.apply(context, args.concat(bindArgs);
    }
    //保持原型链关系
    fBind.prototype = this.prototype;
    //返回函数
    return fBind;
}    

增加考虑项: 调用bind方法的非函数以及bind参数为null时

// 结合上面的例子代码如下
Function.prototype.MyBind = function(context){
    // bind方法调用者非函数时 
    if(typeof this !== 'function'){
        throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }
    //context传入为null时
    if(typeof context === "undefined" || context === null){
        context = window;
    } 
    // 保存绑定时的this指向 introduce.bind(person)
    let self = this;
    // 获取预制参数,若introduce.bind(person, {***}),***则为预制参数
    // 获取arguments除第一个参数context后的其他参数 
    let args = Array.prototype.slice.call(arguments, 1);
    let fBind = function(){
        // 获取bind后函数执行时的参数,便与跟预制参数合并
        let bindArgs = Array.prototype.slice.call(arguments);
        // 改变this指向,传入合并参数 
        return self.apply(context, args.concat(bindArgs);
    }
    //保持原型链关系
    fBind.prototype = this.prototype;
    //返回函数
    return fBind;
}  

五、实际应用场景

场景1:类数组转为真实数组

// 获取的所有div是类数组,没有map、filter等方法
const divs = document.querySelectorAll('div');
// 传统转换方式
const divArray = Array.prototype.slice.call(divs);
// 或
const divArray = [].slice.call(divs);
// 现代写法(ES6+)
const modernArray = Array.from(divs);
//或
const spreadArray = [...divs];

场景2:实现构造函数继承

// 父类 - 被继承者 
function Parent(name) {
    this.name = name;
    this.sayHi = function(){
      console.log(`${name}, 你好`);
    }
}
Parent.prototype.sayYes = function(){
  console.log("YES");
}
// 子类 - 继承者 
function Child(name) {
    // 调用父类构造函数
    Parent.call(this, name);
}
const myChild = new Child("小王");
myChild.sayHi();  // 小王,你好
myChild.sayYes(); // myChild.sayYes is not a function(仅继承构造函数)

场景3:函数借用(需要在一个对象上使用另一个对象的方法)

// 数据验证场景
const validators = {
    isPhone: function(value){
        return /^1[3-9]\d{9}$/.test(value);
    }
}
const myPhone = '13800138000';
// 借用方法调用
if(validators.isPhone.call(null, myPhone)){
    console.log("电话格式正确")
}
// 更优雅写法
const phoneValidator = validators.isPhone.bind(null);
if(phoneValidator(myPhone)){
    console.log("电话格式正确")
};

场景4:函数柯里化与参数预设(需要创建具有预设参数的函数版本)

// 日志工具函数
function log(level, message, context) {
    const timestamp = new Date().toISOString();
    console.log(`[${timestamp}] [${level}] ${message}`, context);
}

// 创建特定级别的日志函数
const error = log.bind(null, 'ERROR');
const warn = log.bind(null, 'WARN');
// 使用预设函数
error('数据库连接失败', { db: 'mysql' });
// 输出: [2025-11-21T10:00:00.000Z] [ERROR] 数据库连接失败 {db: 'mysql'}
warn('内存使用过高', { usage: '85%' });
// 输出: [2025-11-21T10:00:00.000Z] [WARN] 内存使用过高 {usage: '85%'}

场景5:性能优化:避免重复创建函数(在循环或频繁调用处创建新函数影响性能)

// 不好的写法:每次循环都创建新函数
function processItems(items) {
    items.forEach(item => {
        element.addEventListener('click', function() {
            this.doSomething(item.id); // 每次创建新函数
        });
    });
}
// 优化后写法:使用bind预先绑定
function createClickHandler(id) {
    return function() {
        this.doSomething(id);
    };
}
function optimizedProcessItems(items) {
    const handlers = items.map(item => 
        createClickHandler(item.id).bind(this)
    );
    items.forEach((item, index) => {
        element.addEventListener('click', handlers[index]);
    });
}

场景6:第三方库的方法借用(需要在自定义对象上使用库提供的方法)

// 使用lodash的方法,但不想污染全局对象
const _ = require('lodash');
const myUtils = {
    deepClone: _.cloneDeep.bind(_),
    debounce: _.debounce.bind(_),
    throttle: _.throttle.bind(_)
};
// 现在可以像自己的方法一样使用
const clonedObj = myUtils.deepClone(originalObj);
const debouncedFn = myUtils.debounce(() => {}, jie300);

六、总结与思考

今天我们一起:
✅ 理解了面向对象的核心思想
✅ 掌握了参数传递的机制
✅ 攻克了this指向的难题
✅ 实现了call/apply/bind方法

重要提醒:虽然现在有箭头函数、扩展运算符等新特性,但理解这些底层机制能让你在遇到复杂问题时游刃有余。

七、下期预告

下一次我们将深入探讨原型和原型链,这是JavaScript面向对象的灵魂所在!掌握了原型链,你才能真正理解JavaScript的设计哲学。

如果觉得有帮助,请关注+收藏+点赞,这是对我最大的支持!如有问题,可在评论区留言哟

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序媛小王ouc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值