mathjs/src/utils/object.js

258 lines
6.1 KiB
JavaScript

'use strict'
const isBigNumber = require('./bignumber/isBigNumber')
/**
* Clone an object
*
* clone(x)
*
* Can clone any primitive type, array, and object.
* If x has a function clone, this function will be invoked to clone the object.
*
* @param {*} x
* @return {*} clone
*/
exports.clone = function clone (x) {
const type = typeof x
// immutable primitive types
if (type === 'number' || type === 'string' || type === 'boolean' ||
x === null || x === undefined) {
return x
}
// use clone function of the object when available
if (typeof x.clone === 'function') {
return x.clone()
}
// array
if (Array.isArray(x)) {
return x.map(function (value) {
return clone(value)
})
}
if (x instanceof Date) return new Date(x.valueOf())
if (isBigNumber(x)) return x // bignumbers are immutable
if (x instanceof RegExp) throw new TypeError('Cannot clone ' + x) // TODO: clone a RegExp
// object
return exports.map(x, clone)
}
/**
* Apply map to all properties of an object
* @param {Object} object
* @param {function} callback
* @return {Object} Returns a copy of the object with mapped properties
*/
exports.map = function (object, callback) {
const clone = {}
for (const key in object) {
if (exports.hasOwnProperty(object, key)) {
clone[key] = callback(object[key])
}
}
return clone
}
/**
* Extend object a with the properties of object b
* @param {Object} a
* @param {Object} b
* @return {Object} a
*/
exports.extend = function (a, b) {
for (const prop in b) {
if (exports.hasOwnProperty(b, prop)) {
a[prop] = b[prop]
}
}
return a
}
/**
* Deep extend an object a with the properties of object b
* @param {Object} a
* @param {Object} b
* @returns {Object}
*/
exports.deepExtend = function deepExtend (a, b) {
// TODO: add support for Arrays to deepExtend
if (Array.isArray(b)) {
throw new TypeError('Arrays are not supported by deepExtend')
}
for (const prop in b) {
if (exports.hasOwnProperty(b, prop)) {
if (b[prop] && b[prop].constructor === Object) {
if (a[prop] === undefined) {
a[prop] = {}
}
if (a[prop].constructor === Object) {
deepExtend(a[prop], b[prop])
} else {
a[prop] = b[prop]
}
} else if (Array.isArray(b[prop])) {
throw new TypeError('Arrays are not supported by deepExtend')
} else {
a[prop] = b[prop]
}
}
}
return a
}
/**
* Deep test equality of all fields in two pairs of arrays or objects.
* @param {Array | Object} a
* @param {Array | Object} b
* @returns {boolean}
*/
exports.deepEqual = function deepEqual (a, b) {
let prop, i, len
if (Array.isArray(a)) {
if (!Array.isArray(b)) {
return false
}
if (a.length !== b.length) {
return false
}
for (i = 0, len = a.length; i < len; i++) {
if (!exports.deepEqual(a[i], b[i])) {
return false
}
}
return true
} else if (a instanceof Object) {
if (Array.isArray(b) || !(b instanceof Object)) {
return false
}
for (prop in a) {
// noinspection JSUnfilteredForInLoop
if (!exports.deepEqual(a[prop], b[prop])) {
return false
}
}
for (prop in b) {
// noinspection JSUnfilteredForInLoop
if (!exports.deepEqual(a[prop], b[prop])) {
return false
}
}
return true
} else {
return (a === b)
}
}
/**
* Test whether the current JavaScript engine supports Object.defineProperty
* @returns {boolean} returns true if supported
*/
exports.canDefineProperty = function () {
// test needed for broken IE8 implementation
try {
if (Object.defineProperty) {
Object.defineProperty({}, 'x', { get: function () {} })
return true
}
} catch (e) {}
return false
}
/**
* Attach a lazy loading property to a constant.
* The given function `fn` is called once when the property is first requested.
* On older browsers (<IE8), the function will fall back to direct evaluation
* of the properties value.
* @param {Object} object Object where to add the property
* @param {string} prop Property name
* @param {Function} fn Function returning the property value. Called
* without arguments.
*/
exports.lazy = function (object, prop, fn) {
if (exports.canDefineProperty()) {
let _uninitialized = true
let _value
Object.defineProperty(object, prop, {
get: function () {
if (_uninitialized) {
_value = fn()
_uninitialized = false
}
return _value
},
set: function (value) {
_value = value
_uninitialized = false
},
configurable: true,
enumerable: true
})
} else {
// fall back to immediate evaluation
object[prop] = fn()
}
}
/**
* Traverse a path into an object.
* When a namespace is missing, it will be created
* @param {Object} object
* @param {string} path A dot separated string like 'name.space'
* @return {Object} Returns the object at the end of the path
*/
exports.traverse = function (object, path) {
let obj = object
if (path) {
const names = path.split('.')
for (let i = 0; i < names.length; i++) {
const name = names[i]
if (!(name in obj)) {
obj[name] = {}
}
obj = obj[name]
}
}
return obj
}
/**
* A safe hasOwnProperty
* @param {Object} object
* @param {string} property
*/
exports.hasOwnProperty = function (object, property) {
return object && Object.hasOwnProperty.call(object, property)
}
/**
* Test whether an object is a factory. a factory has fields:
*
* - factory: function (type: Object, config: Object, load: function, typed: function [, math: Object]) (required)
* - name: string (optional)
* - path: string A dot separated path (optional)
* - math: boolean If true (false by default), the math namespace is passed
* as fifth argument of the factory function
*
* @param {*} object
* @returns {boolean}
*/
exports.isFactory = function (object) {
return object && typeof object.factory === 'function'
}