Hooks

A JavaScript hook/reply ultility

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.greasyfork.org/scripts/18715/661566/Hooks.js

// ==UserScript==
// @name			Hooks
// @namespace		xuyiming.open@outlook.com
// @description		A JavaScript hook/reply ultility
// @author			xymopen
// @version			1.1.5
// @grant			none
// @license			BSD 2-Clause
// ==/UserScript==

// @updateURL		https://raw.githubusercontent.com/xymopen/JS_Misc/master/Hooks.js

// I was finishing unit test and found there are some bugs about the logic

/**
 * Hooks functions or properties, getters/setters
 * and methods of an object you are intrested in
 */

var Hooks = ( function () {
	"use strict";

	var Functions = {
		/**
		 * wrapper a function
		 * @param {Function} target
		 * @param {(Function, Object?, Arguments) => void} onApply
		 */
		apply: function apply( target, onApply ) {
			if ( "function" === typeof target && "function" === typeof onApply ) {
				var fn = function () {
					return onApply.call( this, target, this, arguments );
				};

				fn.prototype = target.prototype;

				return fn;
			} else {
				throw new TypeError();
			}
		}
	};

	/**
	 * Hooks functions or properties, getters/setters
	 * and methods of an object you are intrested in
	 */
	var Hooks = {
		/**
		 * hook a property of an object
		 * @param {Object} target				- an object having or to have the property to be hooked
		 * @param {String} propertyName			- the name of the property to be hooked
		 * @param {Function} onGet					- the hook call when about to get the property
		 * @param {Function} onSet					- the hook call when about to set the property
		 */
		property: function property( target, propertyName, onGet, onSet ) {
			var descriptor, oldValue;

			if ( Object.prototype.hasOwnProperty.call( target, propertyName ) ) {
				descriptor = Object.getOwnPropertyDescriptor( target, propertyName );

				if ( Object.prototype.hasOwnProperty.call( descriptor, "value" ) ) {
					oldValue = descriptor.value;

					delete descriptor.value;
					delete descriptor.writable;
				} else if ( Object.prototype.hasOwnProperty.call( descriptor, "get" ) ) {
					oldValue = descriptor.get.call( target );
				} else {
					oldValue = undefined;
				}
			} else {
				descriptor = {
					configurable: true,
					enumerable: true,
				};

				oldValue = undefined;
			}

			descriptor.get = function get() {
				return onGet.call( this, target, propertyName, oldValue );
			};

			descriptor.set = function set( newValue ) {
				oldValue = onSet.call( this, target, propertyName, oldValue, newValue );
			};

			Object.defineProperty( target, propertyName, descriptor );
		},
		/**
		 * the hook call when about to get the property
		 * @callback onGet
		 * @param {object} target				- the object having the property hooked
		 * @param {string} propertyName			- the name of the property hooked
		 * @param {any} oldValue				- the current value of the property
		 */

		/**
		 * the hook call when about to set the property
		 * @callback onSet
		 * @param {object} target				- the object having the property hooked
		 * @param {string} propertyName			- the name of the property hooked
		 * @param {any} oldValue				- the current value of the property
		 * @param {any} newValue				- the value about to be set to the property
		 */

		/**
		 * alias of #property but fill the #onSet automatically
		 * @function get
		 * @param {Object} target				- an object having or to have the property to be hooked
		 * @param {String} propertyName			- he name of the property to be hooked
		 * @param {onGet} onGet					- the hook call when about to get the property
		 */
		get: function get( target, propertyName, onGet ) {
			return Hooks.property( target, propertyName, onGet, function ( target, propertyName, oldValue, newValue ) {
				return Hooks.Reply.set( arguments );
			} );
		},

		/**
		 * alias of #property but fill the #onGet automatically
		 * @function set
		 * @param {Object} target				- an object having or to have the property to be hooked
		 * @param {String} propertyName			- the name of the property to be hooked
		 * @param {onSet} onSet					- the hook call when about to set the property
		 */
		set: function set( target, propertyName, onSet ) {
			return Hooks.property( target, propertyName, function ( target, propertyName, oldValue ) {
				return Hooks.Reply.get( arguments );
			}, onSet );
		},

		/**
		 * hook a getter property of an object
		 * @function getter
		 * @param {object} target				- an object having the getter property to be hooked
		 * @param {string} propertyName			- the name of the getter property to be hooked
		 * @param {onGetter} onGetter				- the hook replace the getter
		 */
		getter: function getter( target, propertyName, onGetter ) {
			var descriptor;

			if ( Object.prototype.hasOwnProperty.call( target, propertyName ) ) {
				descriptor = Object.getOwnPropertyDescriptor( target, propertyName );

				if ( Object.prototype.hasOwnProperty.call( descriptor, "get" ) ) {
					descriptor.get = Functions.apply( descriptor.get, function ( getter, thisArg, args ) {
						return onGetter.call( this, target, propertyName, getter, thisArg, args );
					} );

					Object.defineProperty( target, propertyName, descriptor );
				}
			}
		},
		/**
		 * the hook replace the getter
		 * @callback onSetter
		 * @param {object} target				- the object having the getter property hooked
		 * @param {string} propertyName			- he name of the getter property hooked
		 * @param {function} getter				- the getter replaced
		 * @param {object|undefined} thisArg	- #this reference
		 * @param {arguments} args				- the arguments pass to the getter, should be #undefined
		 */

		/**
		 * hook a setter property of an object
		 * @function setter
		 * @param {object} target				- an object having the setter property to be hooked
		 * @param {string} propertyName			- the name of the setter property to be hooked
		 * @param {onSetter} onSetter				- the hook replace the setter
		 */
		setter: function setter( target, propertyName, onSetter ) {
			var descriptor;

			if ( Object.prototype.hasOwnProperty.call( target, propertyName ) ) {
				descriptor = Object.getOwnPropertyDescriptor( target, propertyName );

				if ( Object.prototype.hasOwnProperty.call( descriptor, "set" ) ) {
					descriptor.set = Functions.apply( descriptor.set, function ( setter, thisArg, args ) {
						onSetter.call( this, target, propertyName, setter, thisArg, args );
					} );

					Object.defineProperty( target, propertyName, descriptor );
				}
			}
		},
		/**
		 * the hook replace the setter
		 * @callback onSetter
		 * @param {object} target				- the object having the setter property hooked
		 * @param {string} propertyName			- he name of the setter property hooked
		 * @param {function} setter				- the setter replaced
		 * @param {object|undefined} thisArg	- #this reference
		 * @param {arguments} args				- the arguments pass to the setter, should be a right value
		 */

		/**
		 * hook a method of an object
		 * @function method
		 * @param {object} target				- an object having or to have the method to be hooked
		 * @param {string} methodName			- the name of the method to be hooked
		 * @param {onApply} onApply				- the hook replace the method
		 */
		method: function method( target, methodName, onApply ) {
			var method = target[ methodName ];

			function hook( method ) {
				return Functions.apply( method, function ( method, thisArg, args ) {
					return onApply.call( this, target, methodName, method, thisArg, args );
				} );
			};

			if ( "function" === typeof method ) {
				target[ methodName ] = hook( method );
			} else if ( !Object.prototype.hasOwnProperty.call( target, methodName ) ) {
				Object.defineProperty( target, methodName, {
					configurable: true,
					enumerable: true,
					set: function ( value ) {
						Object.defineProperty( target, methodName, {
							configurable: true,
							enumerable: true,
							value: typeof value === "function" ? hook( value ) : value,
							writable: true
						} );
					},
					get: function () {
						return undefined;
					}
				} );
			}
		},
		/**
		 * the hook replace the method
		 * @callback onApply
		 * @param {object} target				- the object having the method hooked
		 * @param {string} methodName			- the name of the method hooked
		 * @param {object|undefined} thisArg	- #this reference
		 * @param {arguments} args				- the arguments pass to the method
		 */
	};

	Hooks.Reply = {
		get: function ( param ) {
			var target = param[ 0 ],
				propertyName = param[ 1 ],
				oldValue = param[ 2 ];

			return oldValue;
		},

		set: function ( param ) {
			var target = param[ 0 ],
				propertyName = param[ 1 ],
				oldValue = param[ 2 ],
				newValue = param[ 3 ];

			return newValue;
		},

		getter: function ( param ) {
			var target = param[ 0 ],
				propertyName = param[ 1 ],
				getter = param[ 2 ],
				thisArg = param[ 3 ],
				args = param[ 4 ];

			return getter.apply( thisArg, args );
		},

		setter: function ( param ) {
			var target = param[ 0 ],
				propertyName = param[ 1 ],
				setter = param[ 2 ],
				thisArg = param[ 3 ],
				args = param[ 4 ];

			setter.apply( thisArg, args );
		},

		method: function method( param ) {
			var target = param[ 0 ],
				methodName = param[ 1 ],
				method = param[ 2 ],
				thisArg = param[ 3 ],
				args = param[ 4 ];

			return method.apply( thisArg, args );
		},
	};

	return Hooks;
} )();