学习3

41 阅读10分钟

1.以下哪个不是DOM的标准属性

a.siblingNode b.firstChild c.parentNode d.children 这段文本是一个关于DOM(文档对象模型)标准属性的选择题,目的是让答题者从给出的四个选项中选出不属于DOM标准属性的那一项。下面对每个选项进行分析:

选项a:siblingNode

在DOM标准中,并没有siblingNode这个属性。不过,有previousSiblingnextSibling属性,它们分别用于获取当前节点的前一个兄弟节点和后一个兄弟节点。

选项b:firstChild

firstChild是DOM的标准属性,它用于获取当前节点的第一个子节点。例如,在一个HTML元素节点中,使用firstChild可以获取到该元素的第一个子元素或者文本节点。

选项c:parentNode

parentNode也是DOM的标准属性,它用于获取当前节点的父节点。通过这个属性,我们可以在DOM树中向上导航,找到当前节点的父元素。

选项d:children

children同样是DOM的标准属性,它返回一个包含当前元素所有子元素节点的HTMLCollection。需要注意的是,它只包含元素节点,不包含文本节点等其他类型的节点。

综上所述,答案是选项a,因为siblingNode不是DOM的标准属性。

编程题 手写Promise.any

Promise.any 是 ES2021 引入的一个新特性,它接收一个可迭代对象(通常是数组),该对象包含多个 Promise,只要其中任何一个 Promise 成功解决(fulfilled),Promise.any 返回的 Promise 就会以该 Promise 的解决值解决。如果所有的 Promise 都被拒绝(rejected),则返回的 Promise 会以一个 AggregateError 拒绝,该错误包含所有被拒绝的原因。

以下是手写 Promise.any 的实现代码:

javascript

function promiseAny(promises) {
    return new Promise((resolve, reject) => {
        let errors [];
        let completedCount = 0;

        if (promises.length === 0) {
            return reject(new AggregateError([], 'All promises were rejected'));
        }

        promises.forEach((promise, index) => {
            Promise.resolve(promise)
              .then((value) => {
                    resolve(value);
                })
              .catch((error) => {
                    errors[index] = error;
                    completedCount++;
                    if (completedCount === promises.length) {
                        reject(new AggregateError(errors, 'All promises were rejected'));
                    }
                });
        });
    });
}

// 使用示例
const promise1 = Promise.reject(new Error('Error 1'));
const promise2 = Promise.resolve('Success');
const promise3 = Promise.reject(new Error('Error 2'));

promiseAny([promise1, promise2, promise3])
  .then((value) => {
        console.log('Resolved with value:', value);
    })
  .catch((error) => {
        console.error('Rejected with error:', error);
    });

代码解释:

  1. 返回一个新的 PromisepromiseAny 函数返回一个新的 Promise 对象,这是 Promise.any 的基本要求。

  2. 处理空数组情况:如果传入的 promises 数组为空,直接拒绝该 Promise,并抛出一个 AggregateError

  3. 遍历 Promise 数组:使用 forEach 方法遍历传入的 promises 数组。

  4. 将每个元素转换为 Promise:使用 Promise.resolve 将每个元素转换为 Promise,确保传入的元素可以被正确处理。

  5. 处理 Promise 结果

    • 如果任何一个 Promise 成功解决,调用 resolve 方法并传入该 Promise 的解决值。
    • 如果一个 Promise 被拒绝,将错误信息存储在 errors 数组中,并增加 completedCount
    • 当所有的 Promise 都被处理后,如果 completedCount 等于 promises 数组的长度,说明所有的 Promise 都被拒绝,此时调用 reject 方法并抛出一个 AggregateError,包含所有被拒绝的原因。

通过上述代码和解释,你可以实现一个简单的 Promise.any 函数。

考察正则的,实现一个模版引擎 (类似EJS、Pug)

下面为你实现一个简单的模板引擎,其原理是借助正则表达式对模板字符串里的占位符进行匹配,然后用实际的数据替换这些占位符。

代码实现

javascript

function templateEngine(template, data) {
    // 定义正则表达式,用于匹配 {{ }} 包裹的变量名
    const regex = /{{([^}]+)}}/g;

    // 使用 replace 方法替换匹配到的占位符
    return template.replace(regex, (match, variable) => {
        // 去除变量名前后的空格
        variable = variable.trim();
        // 从 data 对象中获取变量的值
        return data[variable] !== undefined ? data[variable] : '';
    });
}

// 使用示例
const template = '你好,{{name}}!你今年 {{age}} 岁了。';
const data = {
    name: '张三',
    age: 25
};

const result = templateEngine(template, data);
console.log(result); 

代码解释

  1. 正则表达式定义/{{([^}]+)}}/g 这个正则表达式的作用是匹配所有被 {{ }} 包裹的内容。其中 ([^}]+) 是一个捕获组,用于捕获 {{ }} 中间的变量名。
  2. replace 方法template.replacegex, callback) 会 template 字符串里查找所有和正则表达式匹配的部分,并且用 callback 函数的返回值替换这些匹配项。
  3. callback 函数:该函数接收两个参数,match 是匹配到的完整字符串(例如 {{name}}),variable 是捕获组里的内容(例如 name)。
  4. 变量值获取:从 data 对象中获取变量的值,若变量存在就返回其值,不存在则返回空字符串。

局限性

  • 此模板引擎仅支持简单的变量替换,不支持条件判断、循环等复杂逻辑。
  • 没有对特殊字符进行转义处理,在处理包含 HTML 标签等特殊字符的数据时可能会有安全问题。

若要实现更复杂的模板引擎,可参考 EJS、Pug 等成熟模板引擎的实现。

实现Promise串行执行,并在失败时重试指定次数的函数。如果所有重试都失败,则整个序列会停止执行

功能概述

要编写一个函数,这个函数的主要作用是让多个 Promise 按照顺序依次执行,也就是串行执行。并且,当某个 Promise 执行失败时,会对这个失败的 Promise 进行指定次数的重试。如果经过指定次数的重试后,这个 Promise 仍然失败,那么整个 Promise 序列就会停止执行,不会再继续执行后续的 Promise

类比理解

可以把这个过程想象成一个流水线作业。每个 Promise 就像是流水线上的一个工序,工序需要按顺序依次完成。如果某个工序出现了问题(对应 Promise 执行失败),会尝试重新做这个工序指定的次数。如果尝试了指定次数后,这个工序还是无法完成,那么整个流水线就会停止工作,不会再进行后续的工序。

代码实现示例

以下是一个实现该功能的 JavaScript 代码示例:

javascript

function promiseSerialWithRetry(promises, maxRetries) {
    let index = 0;

    async function executeWithRetry() {
        if (index >= promises.length) {
            return;
        }
        let retries = 0;
        while (retries < maxRetries) {
            try {
                const result = await promises[index]();
                console.log(`Promise ${index} succeeded:`, result);
                index++;
                return executeWithRetry();
            } catch (error) {
                console.log(`Promise ${index} failed (attempt ${retries + 1}):`, error);
                retries++;
            }
        }
        console.log(`Promise ${index} failed after ${maxRetries} retries. Stopping sequence.`);
    }

    return executeWithRetry();
}

// 示例使用
const promise1 = () => new Promise((resolve, reject) => {
    setTimeout(() => Math.random() > 0.5 ? resolve('Promise 1 success') : reject(new Error('Promise 1 failed')), 100);
});
const promise2 = () => new Promise((resolve, reject) => {
    setTimeout(() => Math.random() > 0.5 ? resolve('Promise 2 success') : reject(new Error('Promise 2 failed')), 100);
});
const promise3 = () => new Promise((resolve, reject) => {
    setTimeout(() => Math.random() > 0.5 ? resolve('Promise 3 success') : reject(new Error('Promise 3 failed')), 100);
});

const promises = [promise1, promise2, promise3];
const maxRetries = 3;

promiseSerialWithRetry(promises, maxRetries)
   .then(() => console.log('All promises executed.'))
   .catch(error => console.error('Sequence stopped due to error:', error));

在这个代码中,promiseSerialWithRetry 函数接收一个 Promise 数组 promises 和最大重试次数 maxRetries 作为参数。函数内部使用递归的方式依次执行每个 Promise,并在失败时进行重试。如果重试次数达到上限仍然失败,就会停止整个序列的执行。

实现一个compile函数,支持多级引用变量以及数组,能够解析嵌套的属性和数组索引

文章中已经给出了两种实现 compile 函数的方法,下面为你详细介绍这两种方法:

方法一:使用正则表达式手动解析

javascript

function compile(str, obj) {
    // 匹配 ${...} 形式的插值表达式
    const exp = /${([^}]+)}/g;

    function matchFn(match, p1) {
        let curObj = obj;
        let value;

        // 将变量路径按 . 分割成多个部分
        p1.split(".").forEach((part) => {
            // 匹配数组索引形式,如 arr[0]
            const arrExp = /(\w+)[(\d+)]/;
            const matchRes = part.match(arrExp);

            if (matchRes) {
                const [_, name, index] = matchRes;
                value = curObj[name] && curObj[name][index];

                if (value && typeof value !== "object") {
                    return value;
                }

                if (!value) throw new Error(`Property not found: ${match}`);

                curObj = value;
            } else {
                value = curObj[part];

                if (value && typeof value !== "object") {
                    return value;
                }

                if (!value) throw new Error(`Property not found: ${match}`);

                curObj = value;
            }
        });

        return value;
    }

    // 使用 matchFn 替换插值表达式
    return str.replace(exp, matchFn);
}

// 示例使用
const template = "Hello, ${user.name}! Your balance is ${user.balance}. You have ${user.items[0]} in your cart. and ${user.items[2].kk}";
const exprObj = {
    user: {
        name: "Alice",
        balance: 100.5,
        items: ["Item1", "Item2", { kk: 1 }],
    },
};
const compiledString = compile(template, exprObj);
console.log(compiledString);

解释

  • 首先使用正则表达式 /${([^}]+)}/g 匹配字符串中所有 ${...} 形式的插值表达式。
  • 对于每个匹配到的插值表达式,将其内部的变量路径按 . 分割成多个部分。
  • 遍历这些部分,如果遇到数组索引形式(如 arr[0]),则解析数组索引并获取对应的值;否则直接对象属性的值。
  • 最后使用 replace 方法将插值表达式替换为实际的值。

方法二:使用 with 函数实现

javascript

function compile(template, context) {
    // 将模板中的插值表达式 {{...}} 替换为 JavaScript 模板字符串语法 ${...}
    const compiledTemplate = template.replace(
        /{{(.*?)}}/g,
        (match, p1) => `${${p1.trim()}}`
    );

    // 动态生成一个函数,用于替换模板中的插值表达式
    const compiledFunction = new Function(
        "context",
        `
        with (context) {
            return `${compiledTemplate}`;
        }
 `
    );

    // 调用函数并返回结果
    return compiledFunction(context);
}

// 示例使用
const template = "Hello, ${user.name}! Your balance is ${user.balance}. You have ${user.items[0]} in your cart. and ${user.items[2].kk}";
const exprObj = {
    user: {
        name: "Alice",
        balance: 100.5,
        items: ["Item1", "Item2", { kk: 1 }],
    },
};
const compiledString = compile(template, exprObj);
console.log(compiledString);

解释

  • 首先使用正则表达式 /{{(.*?)}}/g 将模板中的插值表达式 {{...}} 替换为 JavaScript 模板字符串语法 ${...}
  • 然后使用 new Function 动态生成一个函数,该函数使用 with 语句将上下文对象作为作用域,返回替换后的模板字符串。
  • 最后调用该函数并传入上下文对象,返回最终的编译结果。

需要注意的是,使用 with 函数虽然可以快速实现,但它会影响代码的性能和可读性,并且在严格模式下是不允许使用的。在实际开发中,建议使用方法一

用ES6声明一个class,在es5怎么实现

ES的实现方式

在ES5中没有class关键字,我们需要使用构造函数和原型链来模拟类和继承。

javascript

// 定义父类构造函数
function Parent(name, id) {
    this.id id;
    this.name = name;
    this.list = ['a'];
    this.printName = function() {
        console.log(this.name);
    };
}

// 在父类原型上添加方法
Parent.prototype.sayName = function() {
    console.log(this.name);
};

//实现继承的辅助函数
function inheritPrototype(subClass, superClass) {
    // 复制一份父类的原型
    var p = Object.create(superClass.prototype);
    // 修正构造函数
    p.constructor = subClass;
    // 设置子类原型
    subClass.prototype = p;
}

// 定义子类构造函数
function Child(name, id) {
    // 调用父类构造函数,绑定this到子类实例
    Parent.call(this, name, id);
    
 
}

// 实现子类对父类的继承
inheritPrototype(Child, Parent);

代码解释

  1. 父类构造函数:在ES5中,我们使用普通的函数作为构造函数来模拟类的constructor。在构造函数内部,我们可以为实例添加属性和方法。
  2. 原型方法:通过在构造函数的prototype对象上添加方法,这些方法会被所有实例共享,类似于ES6中类的原型方法。
  3. 继承辅助函数inheritPrototype函数用于实现子类对父类的继承。它通过Object.create方法复制父类的原型,然后修正构造函数,最后将复制的原型赋值给子类的prototype
  4. 子类构造函数:子类构造函数中使用Parent.call(this, name, id)来调用父类的构造函数,确保父类的属性和方法被正确初始化。

分析结果

`const temp = {}

class Test { a(){} b = () => { } c = 1; d = {}; e = temp; static f = temp }

const test1 = new Test(); const test2 = new Test();

console.log(test1.a === test2.a) console.log(test1.b === test2.b) console.log(test1.c === test2.c) console.log(test1.d === test2.d) console.log(test1.e === test2.e) console.log(test1 === test2) console.log(Test.f === Test.f)
`

原因分析:

  1. console.log(test1.a === test2.a); // true

    • a 是一个在 Test 类原型上定义的方法。由于 test1test2 都是 Test 类的实例,它们会共享原型上的方法,因此 test1.atest2.a 指向同一个函数。
  2. console.log(test1.b === test2.b); // false

    • b 是一个通过赋值创建的箭头函数,它是实例自己的属性,而不是原型上的属性。因此,test1.btest2.b 是两个不同的函数实例。
  3. console.log(test1.c === test2.c); // true

    • c 是一个原始值(在这种情况下是数字),原始值是按值传递的,而不是按引用传递。但是,这里比较的是 test1.ctest2.c 的值,由于它们都被赋值为 1,所以比较结果是 true
  4. console.log(test1.d === test2.d); // false

    • d 是一个对象,它是通过赋值创建的实例自己的属性。test1.dtest2.d 是两个不同的对象实例。
  5. console.log(test1.e === test2.e); // true

    • e 被赋值为同一个外部对象 temp 的引用。因此,test1.etest2.e 指向同一个对象。
  6. console.log(test1 === test2); // false

    • test1test2Test 类的两个不同实例,因此它们不相等。
  7. console.log(Test.f === Test.f); // true

    • f 是一个静态属性,它属于 Test 类本身,而不是类的实例。静态属性在类定义时只会被创建一次,因此 Test.f 指向的是同一个对象。

如果a对象引用了b对象,b对象引用了a对象怎么办,引用循环问题

问题分析

当对象之间存在循环引用时,进行深拷贝会陷入无限循环,因为深拷贝会不断尝试复制相互引用的对象。为了解决这个问题,需要使用一个缓存(cache)来记录已经拷贝过的对象,当再次遇到相同对象时,直接从缓存中获取,而不是再次进行拷贝。

代码实现

以下是一个解决循环引用问题的深拷贝函数示例:

javascript

function deepClone(obj, cache = new WeakMap()) {
    // 如果 obj 不是对象,直接返回
    if (typeof obj!== 'object' || obj === null) {
        return obj;
    }
    // 检查缓存中是否已经存在该对象
    if (cache.has(obj)) {
        return cache.get(obj);
    }
    let clone;
    if (Array.isArray(obj)) {
        // 如果是数组,创建一个新数组
        clone = [];
    } else {
        // 如果是普通对象,创建一个新对象
        clone = {}
    }
    // 将当前对象存入缓存
    cache.set(obj, clone);
    // 遍历对象的属性
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            // 递归调用 deepClone 函数拷贝属性值
            clone[key] = deepClone(obj[key], cache);
        }
    }
    return clone;
}

// 测试循环引用
const a = {};
const b = {};
a.b = b;
b.a = a;

const clonedA = deepClone(a);
console.log(clonedA); 

代码解释

  1. 缓存机制:使用 WeakMap 作为缓存 cache,它可以存储对象作为键,并且不会阻止对象被垃圾回收。
  2. 递归拷贝:在深拷贝过程中,递归调用 deepClone 函数来处理对象的属性。
  3. 循环引用处理:在拷贝对象之前,先检查缓存中是否已经存在该对象。如果存在,直接从缓存中获取并返回;如果不存在,将该对象存入缓存,然后继续进行拷贝。

通过这种方式,可以避免循环引用导致的无限循环问题,实现正确的深拷贝。