You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Mobx 4结合jQuery使用时this指向及计算属性异常求助

问题

首次使用Mobx,在非React项目(仅用jQuery更新DOM)中使用Mobx 4(兼容旧浏览器),参考Mobx 2的示例时出现错误。

错误代码

function ShoppingCart() {
    mobx.extendObservable(this, {
        entries: [],
        total: function() {
            return this.entries.reduce(function (sum, entry) {
                return sum + entry.price;
            }, 0);
        }
    });
}

控制台报错

[mobx] Encountered an uncaught exception that was thrown by a reaction or observer component, in: 'Reaction[Autorun@19]' TypeError: Cannot read properties of undefined (reading 'reduce')

尝试箭头函数后的问题

改用箭头函数后:

function ShoppingCart() {
    mobx.extendObservable(this, {
        entries: [],
        total: () => {
            return this.entries.reduce(function (sum, entry) {
                return sum + entry.price;
            }, 0);
        }
    });
}

返回了函数声明内容:0function () { return this.article ? this.article.price * this.amount : 0; }

需解答以下问题:

  • 普通函数与箭头函数的差异
  • Mobx在此场景下如何处理this(.entries)
  • 为何计算属性无法获取entries的值

完整源码

/** Data model */
function Article(name, price) {
    mobx.extendObservable(this, {
        name: name,
        price: price
    });
}

function ShoppingCartEntry(article) {
    mobx.extendObservable(this, {
        article: article,
        amount: 1,
        price: function () {
            return this.article ? this.article.price * this.amount : 0;
        }
    });
}

function ShoppingCart() {
    mobx.extendObservable(this, {
        entries: [],
        total: function () {
            return this.entries.reduce(function (sum, entry) {
                return sum + entry.price;
            }, 0);
        }
    });
}

$(function () {

// Some available articles
    var articles = mobx.observable([
        ["Funny Bunnies", 17.63],
        ["Awesome React", 23.95],
        ["Second hand netbook", 50.00]
    ].map(function (e) {
        return new Article(e[0], e[1]);
    }));

// Our shopping cart
    var shoppingCart = new ShoppingCart();
// With a demo item inside
    shoppingCart.entries.push(new ShoppingCartEntry(articles[0]));

    $.fn.insertAt = function (index, $parent) {
        return this.each(function () {
            if (index === 0) {
                $parent.prepend(this);
            } else {
                $parent.children().eq(index - 1).after(this);
            }
        });
    };

    /** UI Logic */
    var $articles = $("#articles");

// Make the articles list follow the array
    articles.observe(function (change) {
        console.log(change);
        // items where added or removed
        if (change.type === "splice") {
            $articles.children().slice(change.index, change.index + change.removed.length).remove();
            for (var i = 0; i < change.addedCount; i++) {
                renderArticle(articles[change.index + i])
                    .insertAt(change.index + i, $articles);
            }
        }
    }, true); // true makes sure the observe function is invoked immediately

// Render an article in the articles overview, and watch or changes
    function renderArticle(article) {
        var $name = $("<span>").text(article.name);
        var $price = $("<span>").addClass("price").text(article.price);
        mobx.autorun(function () {
            $name.text(article.name);
        });
        mobx.autorun(function () {
            $price.text(article.price);
        });
        return $("<li>").append($name,
            $("<button>").text("+").addClass("add-article"),
            $("<button>").text("edit").addClass("edit-article"),
            $price);
    }

// ShoppingCart ui
    var $cart = $("#cart");
    var $total = $("#total");

    mobx.autorun(function () {
        // console.log(shoppingCart, shoppingCart.total, shoppingCart.entries);
        $total.text(shoppingCart.total);
    }, true);

    shoppingCart.entries.observe(function (change) {
        // items where added or removed
        if (change.type === "splice") {
            $cart.children().slice(change.index, change.index + change.removed.length).remove();
            for (var i = 0; i < change.addedCount; i++) {
                renderCartEntry(shoppingCart.entries[change.index + i])
                    .insertAt(change.index + i, $cart);
            }
        }
    }, true);

    function renderCartEntry(entry) {
        var $name = $("<span>").text(entry.article.name);
        var $amount = $("<span>").addClass("price").text(entry.amount);
        mobx.autorun(function () {
            $name.text(entry.article.name);
        });
        mobx.autorun(function () {
            $amount.text(entry.amount);
        });
        return $("<li>").append(
            $("<button>").text("-").addClass("remove-article"),
            $name,
            $amount);
    }


// Events
    $("#new-article").on("click", function () {
        articles.push(new Article(prompt("Article name"), prompt("Price")));
    });

    $(document).on("click", ".edit-article", function (event) {
        var idx = $(event.target).parent().index();
        articles[idx].name = prompt("New name");
        articles[idx].price = prompt("New price");
    });

    $(document).on("click", ".add-article", function (event) {
        var article = articles[$(event.target).parent().index()];
        var existingEntry = shoppingCart.entries.find(function (entry) {
            return entry.article === article;
        });
        if (existingEntry)
            existingEntry.amount += 1;
        else
            shoppingCart.entries.unshift(new ShoppingCartEntry(article));
    });

    $(document).on("click", ".remove-article", function (event) {
        var entryIndex = $(event.target).parent().index();
        if ((shoppingCart.entries[entryIndex].amount -= 1) < 1)
            shoppingCart.entries.splice(entryIndex, 1);
    });
})

解答

1. 普通函数与箭头函数的核心差异

  • this指向:普通函数的this由调用方式决定;箭头函数没有自身的this,它的this继承自定义时所在的词法作用域。
  • 构造函数兼容性:箭头函数不能作为构造函数使用,无法通过new关键字调用。
  • 原型对象:箭头函数没有原型属性,无法为其添加原型方法。

2. Mobx在extendObservable中对this的处理

在Mobx 4中,直接在extendObservable里定义普通函数作为计算属性时,Mobx会修改函数的this指向——它不会绑定到当前的实例对象,而是指向Mobx内部的观察代理对象,导致this.entries返回undefined,这就是报错的直接原因。

而箭头函数因为自身没有this,它的this指向ShoppingCart构造函数执行时的实例对象,但Mobx 4不会将箭头函数识别为计算属性,而是直接把函数本身当作普通值保存,所以会出现返回函数声明内容的问题。

3. 解决方法:显式使用mobx.computed定义计算属性

Mobx 4调整了计算属性的定义规则,必须显式使用mobx.computed来声明,而不是直接写函数。修改后的ShoppingCart代码如下:

function ShoppingCart() {
    mobx.extendObservable(this, {
        entries: [],
        total: mobx.computed(function() {
            return this.entries.reduce(function (sum, entry) {
                return sum + entry.price;
            }, 0);
        })
    });
}

同时,ShoppingCartEntry中的price属性也需要做同样修改,否则计算总价时会继续出错:

function ShoppingCartEntry(article) {
    mobx.extendObservable(this, {
        article: article,
        amount: 1,
        price: mobx.computed(function () {
            return this.article ? this.article.price * this.amount : 0;
        })
    });
}

4. 迁移说明

Mobx 2中允许直接在extendObservable里用普通函数定义计算属性,但Mobx 4及之后版本要求显式使用computed,这是你从Mobx 2示例迁移过来出现问题的核心原因。

另外,访问computed属性时,Mobx 4会自动调用其内部的getter函数,所以$total.text(shoppingCart.total)可以正常工作,无需手动调用。


内容的提问来源于stack exchange,提问作者Canh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 07:00:58