最近看了一个对Github上面编程语言使用统计的排行榜,JavaScript真可以说是一枝独秀,很难想象20年前,这个语言只是浏览器中的装饰性语言,能做的事情也就是一点特效或者检查一下要提交给服务器的表单是否满足要求。今天的JavaScript已经是一个全栈语言,从客户端到服务器无所不在。很多编程语言都提供了跟JavaScript进行交互的接口,这一点在iOS开发中也不例外。
  iOS7以前,在App中调用JavaScript的方式只有一种,就是通过UIWebView对象的stringByEvaluatingJavaScriptFromString:方法。由于UIWebView中包含了CSS渲染引擎和JavaScript执行引擎(说白了就是微型一个浏览器),因此这个方法可以让UIWebView通过它的JavaScript运行时环境执行JavaScript代码,但是能做的事情非常有限,我们可以先看看下面的例子。

- (void)webViewDidFinishLoad:(UIWebView *)webView {
    // 获得UIWebView中加载页面的标题
    NSString *title = [webView stringByEvaluatingJavaScriptFromString:
        @"document.title"];
    NSLog(@"%@", title);
    // 获得UIWebView中加载页面的链接地址
    NSString *urlStr = [webView stringByEvaluatingJavaScriptFromString:
        @"location.href"];
    NSLog(@"%@", urlStr);
}

  从iOS7开始,我们可以使用JavaScriptCore框架来让我们的Objective-C代码和JavaScript进行深度交互,简单的说我们可以在Objective-C代码中访问JavaScript中的变量或调用JavaScript的函数,也可以JavaScript中使用Objective-C的对象和方法。我们可以先看一个简单的例子。

先加入JavaScriptCore的头文件。

#import <JavaScriptCore/JavaScriptCore.h>

在Objective-C中使用JavaScript的正则表达式验证字符串。

// 创建JavaScript执行环境(上下文)
    JSContext *context = [[JSContext alloc] init];
    NSString *funCode =
        @"var isValidNumber = function(phone) {"
         "    var phonePattern = /^1[34578]\\d{9}$/;"
         "    return phone.match(phonePattern);"
         "};";
    // 执行上面的JavaScript代码
    [context evaluateScript:funCode];
    // 获得isValidNumber函数并传参调用
    JSValue *jsFunction = context[@"isValidNumber"];
    JSValue *value1 = [jsFunction callWithArguments:@[ @"13012345678" ]];
    NSLog(@"%@", [value1 toBool]? @"有效": @"无效");    // 有效
    JSValue *value2 = [jsFunction callWithArguments:@[ @"12345678899" ]];
    NSLog(@"%@", [value2 toBool]? @"有效": @"无效");    // 无效

在Objective-C中调用JavaScript函数求阶乘。

// 创建JavaScript执行环境(上下文)
    JSContext *context = [[JSContext alloc] init];
    // 可以将一个block传给JavaScript上下文
    // 它会被转换成一个JavaScript中的函数
    context[@"factorial"] = ^(int x) {
        double result = 1.0;
        for (; x > 1; x--) {
            result *= x;
        }
        return result;
    };
    // 执行求阶乘的函数
    [context evaluateScript:@"var num = factorial(5);"];
    JSValue *num = context[@"num"];
    NSLog(@"5! = %@", num);    // 5! = 120

  JavaScript和Objective-C中类型的对应关系如下表所示:

Objective-C类型

JavaScript类型

nil

undefined

NSNull

null

NSString

string

NSNumber

number, boolean

NSDictionary

Object object

NSArray

Array object

NSDate

Date object

NSBlock

Function object

id

Wrapper object

Class

Constructor object

  
  再来看一个例子。我们在根视图控制器中放置一个按钮,点击后会导航到下一个视图控制器,其中有一个UIWebView加载了一个网页,页面中有一颗按钮,我们希望点击按钮后导航到上一个视图控制器,要做到这一点,就需要在JavaScript中访问Objective-C对象和方法。

页面的代码:

<!doctype html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>测试页面</title>
        <style type="text/css">
            #backButton { 
                display: inline-block;
                width:50px; height:30px;
            }
        </style>
    </head>

    <body>
        <button id="backButton">返回</button>
        <script type="text/javascript">
            var btn = document.getElementById("backButton");
            var cb = function() {
                window.alert('Hello');
            };
            btn.addEventListener('click', cb, false);
        </script>
    </body>
</html>

第二个视图控制器的代码:

#import "ViewController.h"
#import <JavaScriptCore/JavaScriptCore.h>

@protocol MyProtocol <JSExport>

- (void) letsGoBack;

@end

@interface SecondViewController : ViewController <MyProtocol>

@end
@interface SecondViewController () <UIWebViewDelegate>

@property (weak, nonatomic) IBOutlet UIWebView *myWebViw;

@end

@implementation SecondViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [_myWebViw loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:
        @"http://localhost:8080/myweb/test.html"]]];
    _myWebViw.delegate = self;
}

- (void)webViewDidFinishLoad:(UIWebView *)webView {
    // 通过UIWebView获得网页中的JavaScript执行环境
    JSContext *context = [webView valueForKeyPath:
        @"documentView.webView.mainFrame.javaScriptContext"];
    // 设置处理异常的block回调
    [context setExceptionHandler:^(JSContext *ctx, JSValue *value) {
        NSLog(@"error: %@", value);
    }];

    context[@"callBackObj"] = self;
    // 下面的代码移除了按钮原先绑定的事件回调重新绑定返回上一个视图控制器的代码
    NSString *code =
                   @"var btn = document.getElementById('backButton');"
                    "btn.removeEventListener('click', cb);"
                    "btn.addEventListener('click', function() {"
                    "   callBackObj.letsGoBack();"
                    "});";
    [context evaluateScript:code];
}

// 实现协议中的方法
- (void) letsGoBack {
    // 必须回到主线程刷新用户界面
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.navigationController popViewControllerAnimated:YES];
    });
}

@end

  可以在Github上下载到上面例子的完整代码。