- Published on
BigFrontEnd Category 11 Lodash Methods Implementation Questions
- Authors
- Name
- Yinhuan Yuan
Introduction
This blog post summarizes the Lodash implementation related questions found on BigFrontEnd.Dev.
- 1. Implement Deep Equal - _.isEqual() 2682. DeepEqual
- 2.Implement Deep Clone - _.cloneDeep()
- 3.Implement _.chunk()
- 4.Implement _.once()
- 5.Implement _.get()
- 6.Implement _.partial()
- 7.Implement _.set()
_.isEqual()
2682. DeepEqual
1. Implement Deep Equal - 69.https://bigfrontend.dev/problem/implement-deep-equal-isEqual
_.isEqual is useful when you want to compare complex data types by value not the reference.
Can you implement your own version of deep equal isEqual? The lodash version covers a lot of data types. In this problem, you are asked to support :
primitives plain objects (object literals) array Objects are compared by their own, not inherited, enumerable properties
const a = { a: 'bfe' }
const b = { a: 'bfe' }
isEqual(a, b) // true
a === b // false
const c = [1, a, '4']
const d = [1, b, '4']
isEqual(c, d) // true
c === d // false
Lodash implementation has some strange behaviors. (github issue, like following code
const a = {}
a.self = a
const b = { self: a }
const c = {}
c.self = c
const d = { self: { self: a } }
const e = { self: { self: b } }
lodash.isEqual gives us following result. Notice there is a case that resulting in false.
// result from lodash implementation
_.isEqual(a, b) // true
_.isEqual(a, c) // true
_.isEqual(a, d) // true
_.isEqual(a, e) // true
_.isEqual(b, c) // true
_.isEqual(b, d) // true
_.isEqual(b, e) // false
_.isEqual(c, d) // true
_.isEqual(c, e) // true
_.isEqual(d, e) // true
Setting aside the performance concerns mentioned by lodash, your implement should not have above problem, which means above all returns true and call stack should not exceed the maximum.
Solution:
function deepEqual(obj1, obj2) {
// Check if both arguments are objects
if (typeof obj1 !== 'object' || typeof obj2 !== 'object' || obj1 === null || obj2 === null) {
return obj1 === obj2;
}
// {"0":1} and [1]
if ((Array.isArray(obj1) && !Array.isArray(obj2)) || (!Array.isArray(obj1) && Array.isArray(obj2))) {
return false;
}
// Get the keys of both objects
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
// Check if the number of keys is the same
if (keys1.length !== keys2.length) {
return false;
}
// Check if all keys and values are deeply equal
for (const key of keys1) {
if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) {
return false;
}
}
return true;
}
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };
const obj3 = { a: 1, b: { c: 3 } };
console.log(deepEqual(obj1, obj2)); // Output: true
console.log(deepEqual(obj1, obj3)); // Output: false
_.cloneDeep()
2.Implement Deep Clone - 63.https://bigfrontend.dev/problem/create-cloneDeep
Object.assign() could be used to do shallow copy, while for recursive deep copy, _.cloneDeep could be very useful.
Can you create your own _.cloneDeep()?
The lodash implementation actually covers a lot of data types, for simplicity, your code just need to cover
primitive types and their wrapper Object Plain Objects (Object literal) with all enumerable properties Array There is built-in structuredClone() now, but don't use this to practice
Solution:
function cloneDeep(obj) {
// Check if the input is an object
if (typeof obj !== 'object' || obj === null) {
return obj;
}
// Initialize the result object/array
const result = Array.isArray(obj) ? [] : {};
// Iterate through each key in the input object
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
// Recursively clone nested objects and arrays
result[key] = cloneDeep(obj[key]);
}
}
return result;
}
const obj = {
a: 1,
b: {
c: 2,
d: [3, 4, { e: 5 }]
}
};
const clonedObj = cloneDeep(obj);
console.log(clonedObj); // Output: { a: 1, b: { c: 2, d: [ 3, 4, { e: 5 } ] } }
console.log(clonedObj === obj); // Output: false
_.chunk()
3.Implement 131.https://bigfrontend.dev/problem/implement-lodash-chunk
_.chunk() splits array into groups with the specific size.
Please implement your chunk(arr: any[], size: number)
chunk([1,2,3,4,5], 1)
// [[1], [2], [3], [4], [5]]
chunk([1,2,3,4,5], 2)
// [[1, 2], [3, 4], [5]]
chunk([1,2,3,4,5], 3)
// [[1, 2, 3], [4, 5]]
chunk([1,2,3,4,5], 4)
// [[1, 2, 3, 4], [5]]
chunk([1,2,3,4,5], 5)
// [[1, 2, 3, 4, 5]]
for size smaller than 1, return an empty array.
Solution:
// 2677. Chunk Arra
type JSONValue = null | boolean | number | string | JSONValue[] | { [key: string]: JSONValue };
type Obj = Record<string, JSONValue> | Array<JSONValue>;
function chunk(arr: Obj[], size: number): Obj[][] {
const result: Obj[][] = []
if (size === 0) {
return [];
}
arr.forEach((val: Obj, index: number) => {
if (index % size === 0) {
result.push([val]);
} else {
result.at(-1).push(val);
}
});
return result;
};
/**
* @param {any[]} items
* @param {number} size
* @returns {any[][]}
*/
function chunk(items, size) {
if (size === 0) return []
const ans = []
let cur = []
items.forEach((item) => {
if (cur.length < size) {
cur.push(item)
} else {
ans.push(cur)
cur = [item]
}
})
if (cur.length > 0) {
ans.push(cur)
}
return ans
}
_.once()
4.Implement 46.https://bigfrontend.dev/problem/implement-once
_.once(func) is used to force a function to be called only once, later calls only returns the result of first call.
Can you implement your own once()?
function func(num) {
return num
}
const onced = once(func)
onced(1)
// 1, func called with 1
onced(2)
// 1, even 2 is passed, previous result is returned
Solution:
function once(func) {
let count = 0;
let result;
return function (...args) {
if (count > 0) {
return result;
}
count += 1;
result = func.apply(this, args);
return result;
};
}
need to use func.apply(this, args); rather than func(...args); Otherwise, the following test will fail.
function func(b, c){
return this.a + b + c
}
const onced = once(func)
const obj = {
a: 1,
onced
}
expect(obj.onced(2,3)).toBe(6)
_.get()
5.Implement 85.https://bigfrontend.dev/problem/implement-lodash-get
_.get(object, path, [defaultValue]) is a handy method to help retrieving data from an arbitrary object. if the resolved value from path is undefined, defaultValue is returned.
Please create your own get().
const obj = {
a: {
b: {
c: [1, 2, 3],
},
},
}
get(obj, 'a.b.c') // [1,2,3]
get(obj, 'a.b.c.0') // 1
get(obj, 'a.b.c[1]') // 2
get(obj, ['a', 'b', 'c', '2']) // 3
get(obj, 'a.b.c[3]') // undefined
get(obj, 'a.c', 'bfe') // 'bfe'
Solution:
function get(source, path, defaultValue = undefined) {
path = Array.isArray(path) ? path : path.split(/\.|\[|\]/)
if (path[path.length - 1] === '') path.pop()
if (path.length === 0) return defaultValue
const value = path.reduce((acc, cur) => acc[cur], source)
return value || defaultValue
}
function get(source, path, defaultValue = undefined) {
if (typeof path === 'string') {
path = path.split(/\.|\[|\]/)
}
while (path.length > 0 && path.at(-1) === '') {
path.pop()
}
if (path.length === 0) {
return defaultValue
}
return path.reduce((acc, cur) => (!acc[cur] ? defaultValue : acc[cur]), source)
}
_.partial()
6.Implement 139.https://bigfrontend.dev/problem/implement-partial
_.partial() works like Function.prototype.bind() but this is not bound.
please create your own partial()
const func = (...args) => args
const func123 = partial(func, 1, 2, 3)
func123(4)
// [1,2,3,4]
It should also support placeholder.
const _ = partial.placeholder
const func1_3 = partial(func, 1, _, 3)
func1_3(2, 4)
// [1,2,3,4]
Solution:
function partial(func, ...args) {
// 1, _, 3
const _ = partial.placeholder
return function (...otherArgs) {
// 2, 4
let finalArgs = []
args.forEach((arg) => {
if (arg === _) {
finalArgs.push(otherArgs.shift())
} else {
finalArgs.push(arg)
}
})
finalArgs = finalArgs.concat(otherArgs)
return func.apply(this, finalArgs)
}
}
partial.placeholder = Symbol()
_.set()
7.Implement 156.https://bigfrontend.dev/problem/lodash-set
_.set(object, path, value) is a handy method to updating an object without checking the property existence.
Can you create your own set()
?
const obj = {
a: {
b: {
c: [1, 2, 3],
},
},
}
set(obj, 'a.b.c', 'BFE')
console.log(obj.a.b.c) // "BFE"
set(obj, 'a.b.c.0', 'BFE')
console.log(obj.a.b.c[0]) // "BFE"
set(obj, 'a.b.c[1]', 'BFE')
console.log(obj.a.b.c[1]) // "BFE"
set(obj, ['a', 'b', 'c', '2'], 'BFE')
console.log(obj.a.b.c[2]) // "BFE"
set(obj, 'a.b.c[3]', 'BFE')
console.log(obj.a.b.c[3]) // "BFE"
set(obj, 'a.c.d[0]', 'BFE')
// valid digits treated as array elements
console.log(obj.a.c.d[0]) // "BFE"
set(obj, 'a.c.d.01', 'BFE')
// invalid digits treated as property string
console.log(obj.a.c.d['01']) // "BFE"
Solution:
/**
* @param {object} obj
* @param {string | string[]} path
* @param {any} value
*/
function set(obj, path, value) {
/*
if (typeof path === "string") {
// Replace [xxx] with .xxx
path = path.replace(/\[(\d+)\]/g, ".$1").split(".");
}
*/
if (typeof path === 'string') {
path = path.split(/\.|\[|\]/)
}
while (path.length > 0 && path.at(-1) === '') {
path.pop()
}
if (path.length === 0) {
return
}
const isArrayIndex = (key) => key.match(/^\d+$/) && String(Number(key)) === key
let current = obj
for (let i = 0; i < path.length - 1; i++) {
curKey = path[i]
nextKey = path[i + 1]
if (!current[curKey]) {
if (isArrayIndex(nextKey)) {
current[curKey] = []
} else {
current[curKey] = {}
}
}
current = current[curKey]
}
current[path.at(-1)] = value
}