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

Swift:@dynamicMemberLookup与ExpressibleByStringInterpolation链式调用问题

问题描述

我定义了如下Swift枚举:

@dynamicMemberLookup
enum JSCall: ExpressibleByStringInterpolation {
    case getElement(String)
    case getInnerHTML(id: String)
    case removeAttribute(id: String, attributeName: String)
    case customScript(String)
    case document
    indirect case chainedCall(JSCall, String)

    var value: String {
        switch self {
        case .document:
            return "document"
        case .getElement(let id):
            return JSCall.chainedCall(.document, "getElementById('\(id)')").value
        case .getInnerHTML(let id):
            return JSCall.getElement(id).innerHTML.value // 使用dynamic member lookup
        case .removeAttribute(let id, let attribute):
            return JSCall.chainedCall(.getElement(id), "removeAttribute('\(attribute)')").value
        case .chainedCall(let prefixJs, let suffix):
            return "\(prefixJs.value).\(suffix)" // 修正原代码笔误:scriptString改为value
        case .customScript(let script):
            return script
        }
    }

    init(stringLiteral value: String) {
        self = .customScript(value)
    }
    
    subscript(dynamicMember suffix: String) -> JSCall {
        return .chainedCall(.customScript(self.value), suffix)
    }
}

注:原代码中prefixJs.scriptString为笔误,已修正为prefixJs.value,否则无法正常编译。

我发现计算属性value的getInnerHTML分支里,借助@dynamicMemberLookup可以用.innerHTML的点语法实现链式调用,但尝试带字符串插值的链式调用时,比如:

let call = JSCall.document.getElementById("\(id)").value

会出现编译错误:Cannot call value of non-function type 'JSCall'。我原本以为遵守ExpressibleByStringInterpolation协议能解决这个问题,但并未奏效。请问有没有办法让这个枚举支持自然的方法链式调用语法?


解决方案

仅靠@dynamicMemberLookup只能处理属性式的点语法,无法支持带括号的方法调用。要实现自然的方法链式调用,需要结合**DynamicCallable协议**来处理动态方法调用逻辑。

1. 让枚举遵循DynamicCallable协议

添加扩展实现DynamicCallable,处理带参数的方法调用:

extension JSCall: DynamicCallable {
    func dynamicallyCall(withArguments arguments: [String]) -> JSCall {
        // 从当前JSCall实例中提取基础脚本字符串
        guard let baseScript = self.value as String? else {
            return self
        }
        // 处理参数:将每个参数转为带单引号的字符串,用逗号分隔
        let formattedArgs = arguments.map { "'\($0)'" }.joined(separator: ", ")
        // 拼接成新的调用脚本,包装为customScript类型
        return .customScript("\(baseScript)(\(formattedArgs))")
    }
}

2. 优化dynamicMemberLookup的实现

原来的subscript(dynamicMember:)会把当前实例的value重新包装为customScript,直接返回chainedCall更高效,避免冗余包装:

subscript(dynamicMember suffix: String) -> JSCall {
    return .chainedCall(self, suffix)
}

3. 测试链式调用

现在就可以自然地写出带参数的链式调用代码了:

let id = "testDiv"
// 获取元素innerHTML
let innerHTMLCall = JSCall.document.getElementById("\(id)").innerHTML.value
// 输出:"document.getElementById('testDiv').innerHTML"

// 移除元素属性
let removeAttrCall = JSCall.document.getElementById("\(id)").removeAttribute("class").value
// 输出:"document.getElementById('testDiv').removeAttribute('class')"

额外优化:支持带标签的参数

如果需要支持带标签的方法调用(比如.removeAttribute(id: "test", name: "class")),可以补充实现dynamicallyCall(withKeywordArguments:)方法:

func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, String>) -> JSCall {
    guard let baseScript = self.value as String? else {
        return self
    }
    let formattedArgs = args.map { "\($0.key): '\($0.value)'" }.joined(separator: ", ")
    return .customScript("\(baseScript)(\(formattedArgs))")
}

调用示例:

let call = JSCall.document.getElementById(id: "\(id)").removeAttribute(attributeName: "class").value
// 输出:"document.getElementById(id: 'testDiv').removeAttribute(attributeName: 'class')"

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 12:15:20