18910140161

JavaScript函数劫持思路分析

顺晟科技

2021-06-16 10:39:36

258

最近通过研究微信小程序的源代码,发现了一个很有意思的事情。很多函数都是用surroundByTryCatchFactory的方法包装的,函数调用错误的栈信息会统一格式化打印。这在JS SDK的设计中非常有用。偶然发现这个机制叫JavaScript劫持,于是决定研究一下背后的技术原理。

什么是函数劫持?

函数劫持,顾名思义,就是在函数运行之前劫持一个函数,添加我们想要的函数。当这个函数实际运行的时候,就不再是原来的函数了,而是有了我们添加的函数,这是hook函数的常见原理之一。

一般的劫持原则是一种思路:

1.使用新变量保存要劫持的函数。

2.重写劫持函数的函数。

3.在重写被劫持函数的最后一段(或其他适当的部分)之前,调用该函数。

例如,我们需要多次修改控制台对象以进行日志收集或格式化,如下所示:

从“stacktrace-js”导入stack trace;

让console proxy={ };

let methodList=['log ',' info ',' warn ',' debug ',' error '];

methodList.map(方法={

//备份控制台对象

console proxy[方法]=window . console[方法];

//覆盖控制台代理

window . console[method]=message={

让StackFrame=StackTrace . GetSync()[1];

//格式化控制台信息

让proxyMessage={

level:方法,

message:消息,

location : ` $ { StackFrame . filename } : $ { StackFrame . line number } : $ {

StackFrame.columnNumber

}`

};

console proxy[方法]。调用(这个,proxyMessage);

};

});

这里的劫持功能是通过重写window.console对象方法实现的,但是这里我们污染了原来的对象,有些情况下会出现异常的预期。

那么我们如何判断我们的物体是否在这里被劫持了呢?以console.log为例。正常情况下:

console . log . ToString();

//结果:

(' function log(){[native code]} ');

我们上面的例子将变成:

消息={

让StackFrame=StackTrace . GetSync()[1];

//格式化控制台信息

让proxyMessage={

level:方法,

message:消息,

location : ` $ { StackFrame . filename } : $ { StackFrame . line number } : $ {

StackFrame.columnNumber

}`

};

console proxy[方法]。调用(这个,proxyMessage);

}'

上述对象属性的劫持也可以通过使用Object.defineProperty的setter和getter来实现.

调用、应用、绑定

调用、应用和绑定之间的区别

call()和apply()方法调用具有指定this值和一个或多个单独给定参数的函数,而bind()方法创建一个新函数,在调用时将该关键字设置为提供的值,并在调用新函数时将给定的参数列表作为原始函数参数序列的前几项。

function.call(thisArg,arg1,arg2,)

参数:

ThisArg:函数运行时使用的值。如果此函数处于非严格模式,当指定null或undefined时,它将被自动替换为指向一个全局对象,并且原始值将被包装。

Arg1,arg2,指定参数的列表。

function.apply(thisArg,[argsArray])

参数:

ThisArg:函数运行时使用的值。如果此函数处于非严格模式,当指定null或undefined时,它将被自动替换为指向一个全局对象,并且原始值将被包装。

ArgsArray:可选。数组或类数组对象,其中数组元素将作为单独的参数传递给func函数。如果该参数的值为null或未定义,则意味着不需要传入任何参数。类数组对象可以从ECMAScript 5中使用。

function.bind(thisArg[,arg1[,arg2[,]]])

参数:

ThisArg:调用绑定函数时作为此参数传递给目标函数的值。如果绑定函数是使用新运算符构造的,则忽略该值。当使用bind在setTimeout(作为回调提供)中创建函数时,任何作为thisArg传递的原始值都将被转换为对象。如果绑定函数的参数表为空,那么这个执行范围将被认为是新函数的thisArg。

Arg1,arg2,调用目标函数时,预先添加到绑定函数参数表中的参数。

手写是一种绑定方法

if(!Function.prototype.bind) {

function . prototype . bind=function(this args){

var fn=this,

args=Array . prototype . slice . call(参数,1);

return function() {

return fn.apply(thisArgs,args . concat(slice . call(arguments)));

};

};

}

示例:框架中的函数包装

让我们先来看一段代码:

使用strict ';

let fun=function() {

a=1;

};

fun();

显然,当这样一段代码在‘使用严格’模式下执行时,存在一个未定义的错误。那么我们能否在这种情况下将try-catch统一到异常上并记录下来呢?

/**

*在函数包装框架内试捕

* @param {Function}指定要执行的函数

* @param {string} errMsg错误消息

* @返回{Function}包装函数

*/

函数surroundByTryCatchFactory(fn,errMsg) {

return function() {

尝试{

返回fn.apply(fn,arguments);

} catch(错误){

errMsg=error . message ';\ n ' errMsg

console . error(` FrameWorkScriptError \ n $ { ErrMSg } \ n $ { error . stack } `);

}

};

}

测试:

使用strict ';

let fun=function() {

a=1;

};

fun=surroundbytrywatchfactory(fun,‘错误参数’);

fun();

代理、反映

代理人

让代理=新代理(目标,处理程序);

参数:

目标:用代理包装的目标对象(它可以是任何类型的对象,包括本机数组、函数,甚至是另一个代理)。

处理程序:一个代理对象,它的属性是一个函数,定义了执行操作时代理的行为。

代理操作有13种,下面列出了每种操作的代码名(属性名/方法名)以及触发该操作的方式。请注意,如果没有定义操作,它将被转发到目标对象。

Handler.getPrototypeOf():在读取代理对象的原型时,比如执行Object.getPrototypeOf(代理)时,触发此操作。

Handler.setPrototypeOf():在设置代理对象的原型时,例如在执行object.setprototypeof (proxy,null)时,会触发此操作。

Handler.isExtensible():在判断代理对象是否可扩展时,比如执行对象时,触发此操作。isentensible(代理)。

Handler.preventExtensions():当代理对象变得不可扩展时,例如执行Object.preventExtensions(代理)时,会触发此操作。

handler . getown property descriptor():在获取代理对象的属性的属性描述时,例如在执行object . getown property descriptor(proxy,' foo ')时,会触发此操作。

Handler.defineProperty():定义代理对象的某个属性的属性描述时,比如执行object.defineproperty (proxy,' foo ',{})时,触发此操作。

Handler.has():当判断代理对象是否有属性时,比如在proxy中执行' foo '时,触发此操作。

Handler.get():在读取代理对象的属性时,例如在执行proxy.foo时,触发此操作

Handler.set():当代理对象的属性被赋值时,例如当proxy.foo=1被执行时,这个操作被触发。

Handler.deleteProperty():当删除代理对象的属性时,例如执行delete proxy.foo时,会触发此操作。

Handler.ownKeys():当获取代理对象的所有属性键时,例如执行object . getown property name(proxy)时,会触发此操作。

Handler.apply():当目标对象是函数并被调用时触发。

Handler.construct():当目标对象被构造为构造函数的代理对象时,例如当执行新的proxy()时,会触发此操作。

反射(反射机制)

显示是一个内置的对象,它提供拦截Java脚本语言操作的方法反思不是一个函数对象,因此它是不可构造的。这些方法与处理器对象处理者的方法相同反思对象一共有13 个静态方法。

Reflect.apply(target,thisArg,args)

Reflect.construct(target,args)

Reflect.get(目标、名称、接收者)

反射集(目标、名称、值、接收者)

反射。定义属性(目标,名称,desc)

Reflect.deleteProperty(目标,名称)

反射(目标,名称)

Reflect.ownKeys(目标)

Reflect.isExtensible(目标)

反映。预防措施(目标)

反思。getowntpropertysdescriptor(目标,名称)

Reflect.getPrototypeOf(目标)

反思。设置协议类型(目标,原型)

显示对象的设计目的有这样几个:

将目标对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到显示对象上。

修改某些目标方法的返回结果,让其变得更合理。比如对象定义属性(对象,名称,desc)在无法定义属性时,会抛出一个错误,而反射。定义属性(对象,名称,desc)则会返回假的。

//老写法

尝试{

Object.defineProperty(目标、属性、属性);

//成功

} catch (e) {

//失败

}

//新写法

if (Reflect.defineProperty(目标,属性,属性)){

//成功

} else {

//失败

}

让目标操作都变成函数行为。某些目标操作是命令式,比如目标文件中的名称和删除obj名称],而有(目标,名字)和Reflect.deleteProperty(obj,name)让它们变成了函数行为。

显示对象的方法与代理对象的方法一一对应,只要是代理对象的方法,就能在显示对象上找到对应的方法。这就让代理对象可以方便地调用对应的显示方法,完成默认行为,作为修改行为的基础。也就是说,不管代理怎么修改默认行为,你总可以在显示上获取默认行为。

Vue.js 1.x-2.x版本数据双向绑定机制中数据变动监听是基于Object.defineProperty的吸气剂/设置剂机制实现,3.0 中基于代理进行了改造,下面是数据变动监听核心实现的最简例子:

观察者类{

/**

* 创建数据观察者实例

* @param {Object}数据

*/

构造函数(数据){

if(!数据||数据类型!=='object') {

返回;

}

返回this.proxy(数据);

}

/**

* 代理方法

* @param {*}数据

*/

代理(数据){

返回新代理(数据,{

get:(目标、键、接收器)={

console.log(`data .${key}被读取`);

return Reflect.get(target,key,receiver);

},

set:(目标、键、值、接收器)={

console.log(`data .${key}被赋值`);

return Reflect.set(目标、键、值、接收器);

}

});

}

}

测试:

让数据={

姓名: '赵梦焕,

网站: ' https://赵梦焕。js。“org”

};

让临近数据=新观察者(数据);

控制台。日志(代理数据。姓名);

实例:发布-订阅模式实现

事件发射器类{

constructor() {

这个。事件={ };

这个。next guid=-1;

}

on(类型,侦听器){

const events=this.events

if(!events.hasOwnProperty(类型)){

事件[类型]={ };

}

让guid=' uuid _ ' this.nextGuid

事件[类型][guid]=侦听器;

归还这个;

}

一次(类型,监听器){

函数回调(){

this.off(类型,回调);

Reflect.apply(listener,this,arguments);

}

this.on(类型,回调);

归还这个;

}

关闭(类型,监听器){

const events=this.events

if(!events.hasOwnProperty(类型)){

返回错误的

}

const handlers=events[type];

对象的(字母[键,值]。条目(处理程序)){

if(处理程序。Hasownproperty(key)侦听器===value){

Reflect.deleteProperty(处理程序,键);

返回真实的

}

}

返回错误的

}

发射(类型,args) {

const events=this.events

const handlers=events[type];

对于(让对象的侦听器。值(处理程序)){

Reflect.apply(listener,this,args);

}

}

}

相关文章
我们已经准备好了,你呢?
2024我们与您携手共赢,为您的企业形象保驾护航