当我们用JavaScript编写程序时,我们是否思考过在我们的代码背后发生了什么?如果你正在使用Web应用,那么可能你的代码正在通过一个JS引擎运行。JS引擎是C++编写的,用于执行JavaScript代码。我们的程序在JS引擎初始化后,由app进行管理。这个app可以访问JS引擎内部的js状态,并且可以对其进行改变。这就是原生与js之间通信的底层原理。
在本文中,我们将深入探讨JS引擎的工作原理,尤其是JavaScriptCore,帮助开发者更好地理解它们的工作机制。
JS引擎工作原理
概念
JavaScript引擎是一种特殊的虚拟机,它专门处理JavaScript脚本。我们可以把JavaScript虚拟机理解成一个翻译程序,将人类能够理解的编程语言JavaScript,翻译成机器能够理解的机器语言。这就需要定义规则,这些规则是由ECMAScript定义的。其中ECMAScript 262这份文档,就是对JavaScript这门语言定义了一整套完整的标准。
构成
JS引擎主要由以下几部分构成:
- 编译器:主要工作是将源代码编译成抽象语法树(AST),然后在某些引擎中还包含将抽象语法树转换成字节码。
- 解释器:在某些引擎中,解释器主要是接受字节码,解释执行这个字节码,然后也依赖垃圾回收机制等。
- JIT工具:一个能够JIT(即时编译)的工具,将字节码或者抽象语法树转换成本地代码,当然它也需要依赖垃圾回收器。
- 垃圾回收器和分析工具(profiler) :它们负责垃圾回收和收集引擎中的信息,帮助改善引擎的性能和功效。
工作过程
JavaScript是一种解释型语言,其源代码在执行时会被JavaScript引擎解析和执行。虽然存在多种JavaScript引擎,如V8(由Google开发并用于Chrome浏览器),SpiderMonkey(Mozilla的Firefox使用)和JavaScriptCore(Apple的Safari使用),但它们大体上的工作原理相似。 JS引擎的工作过程主要包括生成语法分析树、生成字节码、执行代码三个阶段。
生成语法分析树
这个阶段包括词法分析和语法分析两个步骤:
- 词法分析:词法分析就是把代码中的字符串分割出来,生成一系列的token。例如,
var a = 2;这段代码会被分解成var,a,=,2,;。 - 语法分析:语法分析的输入就是词法分析的输出,输出是AST抽象语法树, AST是表示token关系的一棵树,它是源代码语法结构的一种抽象表示。例如
var a = 2;中的var,a和=是如何组合在一起的。
生成字节码
生成AST树后,然后根据AST来生成字节码,这一步由编译器来完成。字节码某种程度上就是汇编语言,只是它没有对应特定的CPU。这样的话,生成Bytecode时简单很多,无需为不同的CPU生产不同的代码。
执行代码
字节码 是介于 AST 和 机器码 之间的一种代码,它还是需要通过 解释器 将其转换为 机器码 后才能执行。机器码的执行效率最高。解释器在逐条解释执行字节码时,会分析是否有某段代码被多次执行,这样的代码被称为 热点代码。为了提高这部分代码的执行效率,引擎会把字节码即时编译成机器代码。
在即时编译的过程中,引擎还会尝试进行一些优化,使得生成的机器代码运行更快。这些优化基于一些假设,例如函数的参数类型不变等。如果后续代码的运行违反了这些假设,引擎会废弃优化过的代码,重新回到字节码,这个过程叫做反优化或去优化
JavaScriptCore深入解析
JavaScriptCore是WebKit框架的一部分,它是Apple公司Safari浏览器使用的JavaScript引擎。JavaScriptCore提供了一种高效、安全的方式,使得原生应用程序能够执行JavaScript代码。
核心组件
JavaScriptCore的核心组件主要包括以下三个部分:
- JSVirtualMachine:JSVirtualMachine是JavaScript代码的运行环境,提供了虚拟机的环境。
- JSContext:JSContext可以看作是JSVirtualMachine的一个实例,提供了执行JavaScript代码的环境,同时也是原生代码和JavaScript代码之间通信的桥梁。
- JSValue:JSValue代表了JavaScript代码中的值。它提供了一种方法来转换JavaScript值到原生代码的值,并且反之亦然。
原生与JavaScript之间的通信
JavaScriptCore提供了两种方式,使得原生代码能够与JavaScript代码进行通信:
OC调用JS:原生代码可以直接调用JavaScript代码中的函数和属性。例如,开发者可以通过JSContext的 -evaluateScript: 方法执行JavaScript代码,然后返回一个JSValue对象,这个对象代表了JavaScript代码执行的结果。
这是一个例子:
objcCopy code
JSContext *context = [[JSContext alloc] init];
[context evaluateScript:@"var num = 5 + 5;"];
[context evaluateScript:@"var names = ['Grace', 'Ada', 'Margaret'];"];
[context evaluateScript:@"var triple = function(value) { return value * 3 }"];
JSValue *tripleNum = [context[@"triple"] callWithArguments:@[@5]];
NSLog(@"Tripled: %@", tripleNum.toNumber);
在这个例子中,我们首先创建一个新的JSContext对象。然后,我们使用evaluateScript:方法来执行一些JavaScript代码。最后,我们从JavaScript环境中获取一个函数,然后调用这个函数。
JS调用OC:JavaScript代码也可以调用原生代码。JavaScriptCore提供了两种方式来实现这种交互,一种是通过实现JSExport协议,另一种是通过block。JSExport协议定义了一个原生类能够暴露给JavaScript代码的接口。而block则可以直接作为JavaScript函数的参数被传入。
JavaScriptCore也允许JavaScript代码直接调用Objective-C的方法和属性。这主要是通过使用JSExport协议完成的。
首先,我们需要定义一个协议,这个协议需要继承自JSExport,并声明我们想要暴露给JavaScript的方法和属性:
objcCopy code
@protocol MyObjcProtocol <JSExport>
@property (nonatomic, copy) NSString *name;
- (NSString *)greet;
@end
然后,我们需要创建一个实现了这个协议的类:
objcCopy code
@interface MyObjcClass : NSObject <MyObjcProtocol>
@property (nonatomic, copy) NSString *name;
@end
@implementation MyObjcClass
- (NSString *)greet {
return [NSString stringWithFormat:@"Hello, %@", self.name];
}
@end
最后,我们可以将这个类的一个实例注入到JSContext中,然后在JavaScript代码中直接调用这个对象的方法和属性:
objcCopy code
JSContext *context = [[JSContext alloc] init];
MyObjcClass *obj = [[MyObjcClass alloc] init];
context[@"myObj"] = obj;
[context evaluateScript:@"myObj.name = 'World';"];
[context evaluateScript:@"console.log(myObj.greet());"];
垃圾回收与性能优化
JavaScriptCore的内存管理是基于垃圾回收的。开发者不需要关心内存的分配和回收,因为JavaScriptCore会自动处理这些任务。然而,如果开发者在原生代码中保存了JSValue的引用,那么开发者需要在适当的时机释放这个引用,以防止内存泄漏。
为了提高JavaScript代码的执行性能,JavaScriptCore还引入了JIT(Just-In-Time)编译技术。JIT编译技术可以在运行时将热点代码编译为机器码,从而提高代码的执行效率。
总的来说,JavaScriptCore提供了一种强大而灵活的方式,使得原生应用程序可以与JavaScript代码进行交互。开发者可以使用JavaScriptCore来扩展他们的应用程序的功能,或者提供一种动态脚本的能力。
结语
通过深入了解JS引擎工作原理以及JavaScriptCore的用法,开发者可以更好地理解并运用这些知识到实际开发中,从而更有效地解决问题。