题目一

  1. // 实现一个find函数,并且find函数能够满足下列条件
  2. // title数据类型为string|null
  3. // userId为主键,数据类型为number
  4. // 原始数据
  5. const data = [
  6. {userId: 8, title: 'title1'},
  7. {userId: 11, title: 'other'},
  8. {userId: 15, title: null},
  9. {userId: 19, title: 'title2'}
  10. ];
  11. // 查找data中,符合条件的数据,并进行排序
  12. const result = find(data).where({
  13. "title": /\d$/
  14. }).orderBy('userId', 'desc');
  15. // 输出
  16. console.log(result)
  17. [{ userId: 19, title: 'title2'}, { userId: 8, title: 'title1' }];

解析

在JS代码中,链式调用是非常常见的,如jQuery、Promise等中都使用了链式调用,链式调用是得我们的代码更加的清晰。

链式调用的两种方式

  1. jQuery链式调用是通过return this的形式来实现的,通过对象上的方法最后加上return this,把对象再返回回来,对象就可以继续调用方法,实现链式操作了。 ``javascript const Student = function() {}; Student.prototype.setMathScore = function(math){ this.math = math; return this; } Student.prototype.setEnglishScore = function(english){ this.english = english; return this; } Student.prototype.getMathAndEnglish = function(){ return{math: ${this.math}, english: ${this.english}}`; }

const student = new Student(); const score = student.setMathScore(130).setEnglishScore(118).getMathAndEnglish(); console.log(score); // {math: 130, english: 118}

  1. 2. 我们还可以直接**返回对象本身**来实现链式调用。
  2. ```javascript
  3. const student = {
  4. math: 0,
  5. english: 0,
  6. setMathScore: function(math){
  7. this.math = math;
  8. return this;
  9. },
  10. setEnglishScore: function(english){
  11. this.english = english;
  12. return this;
  13. },
  14. getMathAndEnglish: function(){
  15. return `{math: ${this.math}, english: ${this.english}}`;
  16. }
  17. };
  18. const score = student.setMathScore(10).setEnglishScore(30).getMathAndEnglish();
  19. console.log(score); // {math: 130, english: 118}

当然还有其他实现链式调用的方式,本文就不展开来说了,那么我们来说一说本题吧,很明显本题是一个链式调用,但是和我们上面的介绍又有区别,具体解答可以参考下面的代码。

解答

  1. function find(origin) {
  2. return {
  3. data: origin,
  4. where: function(searchObj) {
  5. const keys = Reflect.ownKeys(searchObj)
  6. for (let i = 0; i < keys.length; i++) {
  7. this.data = this.data.filter(item => searchObj[keys[i]].test(item[keys[i]]))
  8. }
  9. return find(this.data)
  10. },
  11. orderBy: function(key, sorter) {
  12. this.data.sort((a, b) => {
  13. return sorter === 'desc' ? b[key] - a[key] : a[key] - b[key]
  14. })
  15. return this.data
  16. }
  17. }
  18. }

Reflect

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers (en-US)的方法相同。Reflect不是一个函数对象,因此它是不可构造的。
静态方法 Reflect.ownKeys() 返回一个由目标对象自身的属性键组成的数组。

题目二

  1. // 对象的深度比较
  2. // 已知有两个对象obj1和obj2,实现isEqual函数判断对象是否相等
  3. const obj1 = {
  4. a: 1,
  5. c: 3,
  6. b: {
  7. c: [1, 2]
  8. }
  9. }
  10. const obj2 = {
  11. c: 4,
  12. b: {
  13. c: [1, 2]
  14. },
  15. a: 1
  16. }
  17. // isEqual函数,相等输出true,不相等输出false
  18. isEqual(obj1, obj2)

解析

我们知道对象是引用类型,即使看似相同的两个对象也是不相等的

  1. const obj1 = {
  2. a: 1
  3. }
  4. const obj2 = {
  5. b: 1
  6. }
  7. console.log(obj1 === obj2) // false

本题要做的就是判断两个地址不相同的对象是否“相等”,相等的话返回true,否则返回false。本文只给一个参考的解答,实际需要考虑很多方面,可以参考Underscore里的_.isEqual()方法,地址:https://github.com/lessfish/underscore-analysis/blob/master/underscore-1.8.3.js/src/underscore-1.8.3.js#L1094-L1190

解答

  1. // 答案仅供参考
  2. // 更详细的解答建议参考Underscore源码[https://github.com/lessfish/underscore-analysis/blob/master/underscore-1.8.3.js/src/underscore-1.8.3.js#L1094-L1190](https://github.com/lessfish/underscore-analysis/blob/master/underscore-1.8.3.js/src/underscore-1.8.3.js#L1094-L1190)
  3. function isEqual(A, B) {
  4. const keysA = Object.keys(A)
  5. const keysB = Object.keys(B)
  6. // 健长不一致的话就更谈不上相等了
  7. if (keysA.length !== keysB.length) return false
  8. for (let i = 0; i < keysA.length; i++) {
  9. const key = keysA[i]
  10. // 类型不等的话直接就不相等了
  11. if (typeof A[key] !== typeof B[key]) return false
  12. // 当都不是对象的时候直接判断值是否相等
  13. if (typeof A[key] !== 'object' && typeof B[key] !== 'object' && A[key] !== B[key]) {
  14. return false
  15. }
  16. if (Array.isArray(A[key]) && Array.isArray(B[key])) {
  17. if (!arrayEqual(A[key], B[key])) return false
  18. }
  19. // 递归判断
  20. if (typeof A[key] === 'object' && typeof B[key] === 'object') {
  21. if (!isEqual(A[key], B[key])) return false
  22. }
  23. }
  24. return true
  25. }
  26. function arrayEqual(arr1, arr2) {
  27. if (arr1.length !== arr2.length) return false
  28. for (let i = 0; i < arr1.length; i++) {
  29. if (arr1[i] !== arr2[i]) return false
  30. }
  31. return true
  32. }
  33. isEqual(obj1, obj2)

题目三

  1. // 判断JS对象是否存在循环引用
  2. const obj = {
  3. a: 1,
  4. b: 2,
  5. }
  6. obj.c = obj
  7. let obj1 = {};
  8. let obj2 = {
  9. b: obj1
  10. };
  11. obj1.a = obj2;
  12. // isHasCircle函数, 存在环输出true,不存在的话输出false
  13. isHasCircle(obj)

解析

71801dc815ad30659e73312afc9bf802.png
循环引用的判断我们可以通过map来进行暂存,当值是对象的情况下,我们将对象存在map中,循环判断是否存在,如果存在就是存在环了,同时进行递归调用。具体解答可以参考下面的代码。

解答

  1. function isHasCircle(obj) {
  2. let hasCircle = false
  3. const map = new Map()
  4. function loop(obj) {
  5. const keys = Object.keys(obj)
  6. keys.forEach(key => {
  7. const value = obj[key]
  8. if (typeof value == 'object' && value !== null) {
  9. if (map.has(value)) {
  10. hasCircle = true
  11. return
  12. } else {
  13. map.set(value)
  14. loop(value)
  15. }
  16. }
  17. })
  18. }
  19. loop(obj)
  20. return hasCircle
  21. }

思考

https://juejin.cn/post/6904563374873395214
循环引用,垃圾回收策略中引用计数为什么有很大的问题,以及循环引用时的对象在使用 JSON.stringify 时为什么会报错,怎样解决

JS 中引用计数垃圾回收策略的问题

先简单讲一下 JS 中引用垃圾回收策略大体是什么样的一个原理,当一个变量被赋予一个引用类型的值时,这个引用类型的值的引用计数加 1。就像是代码中的 obj1 这个变量被赋予了 obj1 这个对象的地址,obj1 这个变量就指向了这个 obj1(右上)这个对象,obj1(右上)的引用计数就会加1.当变量 obj1的值不再是 obj1(右上)这个对象的地址时,obj1(右上)这个对象的引用计数就会减1.当这个 obj1(右上)对象的引用计数变成 0 后,垃圾收集器就会将其回收,因为此时没有变量指向你,也就没办法使用你了。

看似很合理的垃圾回收策略为什么会有问题呢?

就是上面讲到的循环引用导致的,下面来分析一下。当 obj1 这个变量执行 obj1 这个对象时,obj1 这个对象的引用计数会加 1,此时引用计数值为 1,接下来 obj2 的 b 属性又指向了 obj1 这个对象,所以此时 obj1 这个对象的引用计数为 2。同理 obj2 这个对象的引用计数也为2.
当代码执行完后,会将变量 obj1 和 obj2 赋值为 null,但是此时 obj1 和 obj2 这两个对象的引用计数都为1,并不为 0,所以并不会进行垃圾回收,但是这两个对象已经没有作用了,在函数外部也不可能使用到它们,所以这就造成了内存泄露

在现在广泛采用的标记清除回收策略中就不会出现上面的问题

标记清除回收策略的大致流程是这样的,最开始的时候将所有的变量加上标记,当执行 cycularReference 函数的时候会将函数内部的变量这些标记清除,在函数执行完后再加上标记。这些被清除标记又被加上标记的变量就被视为将要删除的变量,原因是这些函数中的变量已经无法被访问到了。像上述代码中的 obj1 和 obj2 这两个变量在刚开始时有标记,进入函数后被清除标记,然后函数执行完后又被加上标记被视为将要清除的变量,因此不会出现引用计数中出现的问题,因为标记清除并不会关心引用的次数是多少。

循环引用的对象使用 JSON.stringify 为什么会报错

image.png
原因
obj1 这个对象和 obj2 会无限相互引用,JSON.tostringify 无法将一个无限引用的对象序列化为 JOSN 字符串。
下面是 MDN 的解释:

JSON.stringify() 将值转换为相应的JSON格式:

  • 转换值如果有 toJSON() 方法,该方法定义什么值将被序列化。
  • 非数组对象的属性不能保证以特定的顺序出现在序列化后的字符串中。
  • 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。
  • undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。函数、undefined 被单独转换时,会返回 undefined,如JSON.stringify(function(){}) or JSON.stringify(undefined).
  • 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
  • 所有以 symbol 为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们。
  • Date 日期调用了 toJSON() 将其转换为了 string 字符串(同Date.toISOString()),因此会被当做字符串处理。
  • NaN 和 Infinity 格式的数值及 null 都会被当做 null。
  • 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性。

解决方法 - 消除循环引用

一个自然的想法能不能消除循环引用,一个 JSON 扩展包 做到了这一点, 使用 JSON.decycle 可以去除循环引用。为了方便测试我直接在 JSON 扩展包的 Github 仓库中下载了 cycle.js 这个函数,将下面这段代码赋值到最下面,然后利用 node 运行进行测试,问题得到解决,结果如下图所示。

  1. function circularReference() {
  2. let obj1 = {};
  3. let obj2 = {
  4. b: obj1
  5. };
  6. obj1.a = obj2;
  7. let c = JSON.decycle(obj1);
  8. console.log(JSON.stringify(c));
  9. }
  10. circularReference();

运行结果:
阿里笔试编程题 - 图3