SPRINT-7

100 阅读18分钟

249、Js中节流与防抖的实现

  • 节流:是在每个时间段里,最多只允许运行一次。比如说resize调整窗口,在调整窗口的过程中,事件一直在高频率的触发,可以利用节流函数让其在一定的间隔时间段内最多触发一次。
  • 防抖:在高频调用中,只有足够的空闲时间,代码才会执行一次,常见的就是input的change事件,只有停顿输入的事件大于指定的时间,代码才会执行一次。

技术详解

**节流(throttle),**常见的实现方式包括时间戳和定时器两种方式。

时间戳方式:记录上一次执行的时间戳,当当前时间戳 - 上一次执行的时间戳 >= 指定时间间隔时,才执行相应的操作或事件;否则不执行。

function throttle(func, wait) {
  let lastTime = 0;
  return function() {
    const context = this;
    const args = arguments;
    const currentTime = +new Date();
    if (currentTime - lastTime > wait) {
      func.apply(context, args);
      lastTime = currentTime;
    }
  };
}

定时器方式:使用定时器控制函数的执行次数,在指定时间间隔内,如果有多个操作或事件被触发,则只执行最后一次操作或事件。

function throttle(func, wait) {
  let timeout = null;
  return function() {
    const context = this;
    const args = arguments;
    if (!timeout) {
      timeout = setTimeout(function() {
        func.apply(context, args);
        timeout = null;
      }, wait);
    }
  };
}

**防抖(debounce),**防抖是指在一定时间内,只有最后一次操作或事件才会被执行。通过防抖机制,可以限制连续快速的操作或事件的执行次数,从而减少浏览器的计算量、提升页面性能和用户体验。

防抖适用于以下场景:

  • 搜索框输入联想。
  • 频繁的点击或提交操作。
  • 窗口大小调整、滚动等事件的处理。

防抖的实现方式与节流类似,也有时间戳和定时器两种方式。

时间戳方式:记录上一次执行的时间戳,当当前时间戳 - 上一次执行的时间戳 >= 指定时间间隔时,才执行相应的操作或事件;否则重新计时。

function debounce(func, wait) {
  let timeout = null;
  return function() {
    const context = this;
    const args = arguments;
    const currentTime = +new Date();
    if (timeout) clearTimeout(timeout);
    if (currentTime - lastTime > wait) {
      func.apply(context, args);
      lastTime = currentTime;
    } else {
      timeout = setTimeout(function() {
        func.apply(context, args);
      }, wait);
    }
  };
}

定时器方式:使用定时器控制函数的执行,在指定时间间隔内,如果有多个操作或事件被触发,则只执行最后一次操作或事件。

function debounce(func, wait) {
  let timeout = null;
  return function() {
    const context = this;
    const args = arguments;
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(function() {
      func.apply(context, args);
    }, wait);
  };
}

250、Js中的深拷贝与浅拷贝

  • 浅拷贝是指复制一个对象或数组的引用地址,而不是实际的数据内容。简单来说,就是克隆对象的第一层属性,不会克隆嵌套对象的属性。
  • 可以使用 Object.assign()数组 slice() 方法、**数组 concat()、**Spread operator 方法等几种方法实现浅拷贝
  • 深拷贝是指完全复制一个对象或数组,包括其所有的嵌套对象和子属性。
  • 可以使用 JSON.parse() 和 JSON.stringify()(但无法复制函数和 undefined 值)递归拷贝等几种方法实现深拷贝

递归拷贝

function deepClone(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }

  const newObj = Array.isArray(obj) ? [] : {};

  for (let key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      newObj[key] = deepClone(obj[key]);
    }
  }

  return newObj;
}

const obj1 = { a: 1, b: { c: 2 } };
const obj2 = deepClone(obj1);
obj2.b.c = 3;
console.log(obj1.b.c); // 输出 2

注意:对于循环引用的情况,递归拷贝会导致堆栈溢出,因此需要特殊处理。进行深拷贝时,若没有特殊处理,会导致无限递归,最终导致堆栈溢出。因此需要在拷贝对象时,判断是否已经拷贝过该对象,如果已经拷贝则直接返回引用,避免重复拷贝。

function deepClone(obj, hash = new WeakMap()) {
if (typeof obj !== 'object' || obj === null) {
    return obj;
}
if (hash.has(obj)) {
    return hash.get(obj);
}
const newObj = Array.isArray(obj) ? [] : {};
hash.set(obj, newObj);
for (let key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
        newObj[key] = deepClone(obj[key], hash);
    }
}
    return newObj;
}
const obj1 = {};
const obj2 = { a: 1 };
obj1.obj2 = obj2;
obj2.obj1 = obj1;
const obj3 = deepClone(obj1);
console.log(obj3.obj2.a); // 输出 1

JSON.stringify深拷贝的缺点

  • 如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式,而不是对象的形式
  • 如果obj里面有RegExp,则打印出来是空对象
  • 如果对象中有函数或者undefined,则会直接被丢掉
  • 如果json里有对象是由构造函数生成的,则会丢掉对象的constructon

JavaScript 中的深拷贝和浅拷贝技术可以帮助我们在处理对象和数组时更加灵活、方便。浅拷贝只能克隆第一层属性,而深拷贝可以克隆所有嵌套对象和子属性。同时,在实现深拷贝时需要特别注意循环引用的情况,避免无限递归导致堆栈溢出问题。

251、实现一个类型判断的方法

在JS中,我们最常用的判断方法自然是 typeof

以下是实现 typeOf() 方法的代码:

function typeOf(obj) {
  const type = typeof obj;
  
  if(type !== "object") {
    return type;
  }
  
  return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, "$1");
}

// Example Usage:
console.log(typeOf(123));                        // Output: "number"
console.log(typeOf("hello"));                    // Output: "string"
console.log(typeOf(true));                       // Output: "boolean"
console.log(typeOf(undefined));                  // Output: "undefined"
console.log(typeOf(null));                       // Output: "null"
console.log(typeOf({}));                         // Output: "object"
console.log(typeOf([]));                         // Output: "array"
console.log(typeOf(new Date()));                 // Output: "date"
console.log(typeOf(/regex/));                    // Output: "regexp"
console.log(typeOf(function(){}));               // Output: "function"

以上代码中,我们定义了一个 typeOf() 函数来判断数据类型。如果传入的参数 obj 的类型为 "object",则使用 Object.prototype.toString.call() 方法来获取它的具体类型。

252、【树形结构】将树形结构扁平化

在 JavaScript 中,将树形结构扁平化可以使用递归和栈两种方式实现。

递归的实现方式:

function flattenTreeRecursively(tree) {
  if (!tree.children || tree.children.length === 0) {
    return [tree];
  } else {
    let result = [tree];
    for (let i = 0; i < tree.children.length; i++) {
      result = result.concat(flattenTreeRecursively(tree.children[i]));
    }
    return result;
  }
}

// 示例
const tree = {
  value: 1,
  children: [
    {
      value: 2,
      children: [
        {value: 4},
        {value: 5}
      ]
    },
    {
      value: 3,
      children: [
        {value: 6},
        {value: 7},
        {
          value: 8,
          children: [
            {value: 9},
            {value: 10}
          ]
        }
      ]
    }
  ]
};
console.log(flattenTreeRecursively(tree));
// Output: [{value: 1}, {value: 2}, {value: 4}, {value: 5}, {value: 3}, {value: 6}, {value: 7}, {value: 8}, {value: 9}, {value: 10}]

栈的实现方式:

function flattenTreeWithStack(tree) {
  const stack = [tree];
  const result = [];

  while (stack.length > 0) {
    const node = stack.pop();
    result.push(node);

    if (node.children && node.children.length > 0) {
      for (let i = node.children.length - 1; i >= 0; i--) {
        stack.push(node.children[i]);
      }
    }
  }

  return result;
}

// 示例
const tree = {
  value: 1,
  children: [
    {
      value: 2,
      children: [
        {value: 4},
        {value: 5}
      ]
    },
    {
      value: 3,
      children: [
        {value: 6},
        {value: 7},
        {
          value: 8,
          children: [
            {value: 9},
            {value: 10}
          ]
        }
      ]
    }
  ]
};
console.log(flattenTreeWithStack(tree));
// Output: [{value: 1}, {value: 3}, {value: 8}, {value: 10}, {value: 9}, {value: 7}, {value: 6}, {value: 2}, {value: 5}, {value: 4}]

以上代码中实现了一个 flattenTreeWithStack() 函数,使用栈的方式进行扁平化。将根节点加入到栈中,并在循环中反复进行以下步骤:从栈顶取出一个节点,并将其加入到结果数组中;如果该节点有子节点,则逆序遍历子节点并将它们依次加入到栈中。最终,当栈为空时,返回结果数组。

253、【树形结构】根据id 和 parentid 将数组数据转成树形结构

可以使用 JavaScript 中的 reduce 方法配合递归实现将数组转换为树形结构。假设数组中每个元素都包含 idparentId 属性,其中 parentId 表示该元素的父级元素的 id 值,根据这些信息我们可以构建出树形结构。

以下是一个示例代码:

function buildTree(array, parentId = null) {
  return array.reduce((accumulator, currentValue) => {
    if (currentValue.parentId === parentId) {
      const children = buildTree(array, currentValue.id);
      if (children.length > 0) {
        currentValue.children = children;
      }
      accumulator.push(currentValue);
    }
    return accumulator;
  }, []);
}

// 示例数据
const data = [
  { id: 1, name: 'Node 1', parentId: null },
  { id: 2, name: 'Node 1.1', parentId: 1 },
  { id: 3, name: 'Node 1.2', parentId: 1 },
  { id: 4, name: 'Node 1.2.1', parentId: 3 },
  { id: 5, name: 'Node 1.2.2', parentId: 3 },
  { id: 6, name: 'Node 2', parentId: null },
  { id: 7, name: 'Node 2.1', parentId: 6 },
  { id: 8, name: 'Node 2.1.1', parentId: 7 },
  { id: 9, name: 'Node 2.1.2', parentId: 7 },
  { id: 10, name: 'Node 2.2', parentId: 6 }
];

// 转换为树形结构
const tree = buildTree(data);
console.log(tree);

以上代码中定义了一个 buildTree() 函数,接收一个数组作为参数以及可选的 parentId 参数用于递归。在 reduce() 方法中遍历数组,如果找到了当前父级节点,则进行递归查找该节点下的所有子节点,并将其添加到当前节点的 children 属性中。最后返回结果数组。

254、请写至少三种数组去重的方法

  • 利用filter
  • 利用ES6 Set去重(ES6中最常用)
  • 利用for嵌套for,然后splice去重(ES5中最常用)

255、将两个数组合并,去重,不要求排序,返回一维数组

可以使用 Set 数据结构,先将两个数组合并,然后通过 Set 去重,并将结果转换为一维数组返回。

以下是实现代码:

function mergeAndRemoveDuplicates(arr1, arr2) {
  let set = new Set([...arr1, ...arr2])
  return Array.from(set)
}

// Example Usage:
const arr1 = [1, 2, 3, 4]
const arr2 = [3, 4, 5, 6]
console.log(mergeAndRemoveDuplicates(arr1, arr2)) // Output: [1, 2, 3, 4, 5, 6]

256、如何找到数组中出现次数最多的字符串

可以使用对象或 Map 来记录每个字符串出现的次数,然后再遍历该对象或 Map,找到出现次数最多的字符串。

以下是使用对象实现的代码

function findMostFrequent(arr) {
  let obj = {}
  let maxCount = 0
  let maxItem = ''
  for (let item of arr) {
    obj[item] = obj[item] ? obj[item] + 1 : 1
    if (obj[item] > maxCount) {
      maxCount = obj[item]
      maxItem = item
    }
  }
  return maxItem
}

// Example Usage:
const arr = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
console.log(findMostFrequent(arr)) // Output: "apple"

以上代码中,我们定义一个 findMostFrequent 函数,它接收一个字符串数组 arr,采用对象记录每个字符串出现的次数,同时记录出现次数最多的字符串和它的出现次数。最后返回出现次数最多的字符串。

以下是使用 Map 实现的代码

function findMostFrequent(arr) {
  let map = new Map()
  let maxCount = 0
  let maxItem = ''
  for (let item of arr) {
    let count = map.get(item) ?? 0
    map.set(item, count + 1)
    if (count + 1 > maxCount) {
      maxCount = count + 1
      maxItem = item
    }
  }
  return maxItem
}

// Example Usage:
const arr = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
console.log(findMostFrequent(arr)) // Output: "apple"

以上代码中,我们定义一个 findMostFrequent 函数,它接收一个字符串数组 arr,采用 Map 记录每个字符串出现的次数,同时记录出现次数最多的字符串和它的出现次数。最后返回出现次数最多的字符串。

257、手写打乱数组顺序的方法

实现思路:

  • 取出数组的第一个元素,随机产生一个索引值,将该第一个元素和这个索引对应的元素进行交换。

  • 第二次取出数据数组第二个元素,随机产生一个除了索引为1的之外的索引值,并将第二个元素与该索引值对应的元素进行交换

  • 按照上面的规律执行,直到遍历完成

    let arr = [1,2,3,4,5,6,7,8,9,10];
    for (let i = 0; i < arr.length; i++) {
      const randomIndex = Math.round(Math.random() * (arr.length - 1 - i)) + i;
      [arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]];
    }
    console.log(arr)
    

258、实现函数柯里化(Currying)

函数柯里化指的是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

function curry(fn, ...args) {
  return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}

259、实现Js中的 Compose (组合)

组合(Compose)是几个函数组成一个新的函数,这个新的函数用于从右到左执行它的组成函数,并将前一个函数的输出作为后一个函数的输入。

以下是实现 compose() 方法的代码:

function compose(...fns) {
  return function(x) {
    return fns.reduceRight(function(acc, fn) {
      return fn(acc);
    }, x);
  };
}

// Example Usage:
const addOne = x => x + 1;
const double = x => x * 2;
const subtractThree = x => x - 3;

const composedFunc = compose(subtractThree, double, addOne);

console.log(composedFunc(2)); // Output: 0

以上代码中,我们定义了一个 compose() 函数来实现组合。它接收任意数量的函数作为参数,并返回一个新的函数,该函数在执行时按照参数顺序从右到左依次调用组成的函数,将前一个函数的输出作为后一个函数的输入,并返回最终输出结果。

在上面的示例中,我们使用 compose() 函数将三个函数 addOnedoublesubtractThree 组合起来,形成一个新的函数 composedFunc。当 composedFunc 函数被调用并传入参数 2 时,它会依次执行 addOnedoublesubtractThree 函数,并通过函数的返回值完成计算,最终输出 0

260、实现Js中的 Pipe (管道)

管道(Pipe)是几个函数组成一个新的函数,这个新的函数用于从左到右执行它的组成函数,并将前一个函数的输出作为后一个函数的输入。

以下是实现 pipe() 方法的代码:

function pipe(...fns) {
  return function(x) {
    return fns.reduce(function(acc, fn) {
      return fn(acc);
    }, x);
  };
}

// Example Usage:
const addOne = x => x + 1;
const double = x => x * 2;
const subtractThree = x => x - 3;

const pipedFunc = pipe(addOne, double, subtractThree);

console.log(pipedFunc(2)); // Output: 1

以上代码中,我们定义了一个 pipe() 函数来实现管道。它接收任意数量的函数作为参数,并返回一个新的函数,该函数在执行时按照参数顺序从左到右依次调用组成的函数,将前一个函数的输出作为后一个函数的输入,并返回最终输出结果。

在上面的示例中,我们使用 pipe() 函数将三个函数 addOnedoublesubtractThree 连接起来,形成一个新的函数 pipedFunc。当 pipedFunc 函数被调用并传入参数 2 时,它会依次执行 addOnedoublesubtractThree 函数,并通过函数的返回值完成计算,最终输出 1

261、实现斐波那契数列

斐波那契数列(Fibonacci sequence),也称黄金分割数列,是指这样一个数列:0、1、1、2、3、5、8、13、21、34、…… 

递归方式:

function fibonacci(num) {
  if (num <= 1) {
    return num;
  } else {
    return fibonacci(num - 1) + fibonacci(num - 2);
  }
}

// 示例
console.log(fibonacci(10)); // Output: 55

迭代方式:

function fibonacci(num) {
  let a = 0;
  let b = 1;

  for (let i = 2; i <= num; i++) {
    const temp = b;
    b = a + b;
    a = temp;
  }

  return b;
}

// 示例
console.log(fibonacci(10)); // Output: 55

以上代码中,第一种递归方式的 fibonacci() 函数使用了递归算法来计算斐波那契数列,当输入值小于等于 1 的时候返回原值,否则递归调用 fibonacci() 函数,并将输入值减 1 和减 2 的结果相加,得到最终的斐波那契数列值。

而第二个迭代方式的 fibonacci() 函数使用了迭代算法来计算斐波那契数列,当输入值大于等于 2 的时候使用 for 循环进行迭代计算,直到得出最终的斐波那契数列值。

262、Js中如何判断括号的正确性

在 JavaScript 中,可以使用栈来判断括号字符串的正确性。以下是一个示例代码:

function isValid(str) {
  const stack = [];

  for (let i = 0; i < str.length; i++) {
    const ch = str.charAt(i);

    if (ch === '(' || ch === '[' || ch === '{') {
      stack.push(ch);
    } else if (ch === ')' && stack.length > 0 && stack[stack.length - 1] === '(') {
      stack.pop();
    } else if (ch === ']' && stack.length > 0 && stack[stack.length - 1] === '[') {
      stack.pop();
    } else if (ch === '}' && stack.length > 0 && stack[stack.length - 1] === '{') {
      stack.pop();
    } else {
      return false;
    }
  }

  return stack.length === 0;
}

// 示例
console.log(isValid('()[]{}')); // Output: true
console.log(isValid('(]')); // Output: false

上代码中,isValid() 函数接收一个字符串作为参数,使用一个栈来记录遇到的左括号,当遇到右括号时,判断栈顶是否匹配,如果匹配则出栈,否则返回 false

具体实现中,我们对三种不同的左括号分别入栈,遇到右括号时分别进行匹配,并将匹配成功的左括号出栈。如果整个字符串扫描完后栈为空,则说明所有左括号都有对应的右括号,返回 true,否则返回 false

263、用Promise实现图片的异步加载

let imageAsync=(url)=>{
            return new Promise((resolve,reject)=>{
                let img = new Image();
                img.src = url;
                img.οnlοad=()=>{
                    console.log(`图片请求成功,此处进行通用操作`);
                    resolve(image);
                }
                img.οnerrοr=(err)=>{
                    console.log(`失败,此处进行失败的通用操作`);
                    reject(err);
                }
            })
        }
        
imageAsync("url").then(()=>{
    console.log("加载成功");
}).catch((error)=>{
    console.log("加载失败");
})

264、实现Object.defineProperty(简易版)

Vue2的响应式原理,结合了Object.defineProperty的数据劫持,以及发布订阅者模式
Vue2的数据劫持,就是通过递归遍历data里的数据,用Object.defineProperty给每一个属性添加getter和setter,
并且把data里的属性挂载到vue实例中,修改vue实例上的属性时,就会触发对应的setter函数,向Dep订阅器发布更新消息,
对应的Watcher订阅者会收到通知,调用自身的回调函数,让编译器去更新视图。

    const obj = {
      name: 'bamboo',
      age: 20
    }
    const p = {}
    for (let key in obj) {
      Object.defineProperty(p, key, {
        get() {
          console.log(`有人读取p里的${key}属性`);
          return obj[key]
        },
        set(val) {
          console.log(`有人修改了p里的${key}属性,值为${val},需要去更新视图`);
          obj[key] = val
        }
      })
    }

265、实现Proxy数据劫持(简易版)

 // Vue3的数据劫持通过Proxy函数对代理对象的属性进行劫持,通过Reflect对象里的方法对代理对象的属性进行修改,
 // Proxy代理对象不需要遍历,配置项里的回调函数可以通过参数拿到修改属性的键和值
 // 这里用到了Reflect对象里的三个方法,get,set和deleteProperty,方法需要的参数与配置项中回调函数的参数相同。
 // Reflect里的方法与Proxy里的方法是一一对应的,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。
   const obj = {
      name: 'bamboo',
      age: 20
    }
   const p = new Proxy(obj, {
      // 读取属性的时候会调用getter
      get(target, propName) {  //第一个参数为代理的源对象,等同于上面的Obj参数。第二个参数为读取的那个属性值
        console.log(`有人读取p对象里的${propName}属性`);
        return Reflect.get(target, propName)
      },
      // 添加和修改属性的时候会调用setter
      set(target, propName, value) { //参数等同于get,第三个参数为修改后的属性值
        console.log(`有人修改了p对象里的${propName}属性,值为${value},需要去修改视图`);
        Reflect.set(target, propName, value)
      },
      // 删除属性时,调用deleteProperty
      deleteProperty(target, propName) { // 参数等同于get
        console.log(`有人删除了p对象里的${propName}属性,需要去修改视图`);
        return Reflect.deleteProperty(target, propName)
      }
    })

266、实现路由(简易版)

// hash路由
class Route{
  constructor(){
    // 路由存储对象
    this.routes = {}
    // 当前hash
    this.currentHash = ''
    // 绑定this,避免监听时this指向改变
    this.freshRoute = this.freshRoute.bind(this)
    // 监听
    window.addEventListener('load', this.freshRoute, false)
    window.addEventListener('hashchange', this.freshRoute, false)
  }
  // 存储
  storeRoute (path, cb) {
    this.routes[path] = cb || function () {}
  }
  // 更新
  freshRoute () {
    this.currentHash = location.hash.slice(1) || '/'
    this.routes[this.currentHash]()
  }
}

267、使用 setTimeout 实现 setInterval

function mySetInterval(fn, timeout) {
  // 控制器,控制定时器是否继续执行
  var timer = {
    flag: true
  };
  // 设置递归函数,模拟定时器执行。
  function interval() {
    if (timer.flag) {
      fn();
      setTimeout(interval, timeout);
    }
  }
  // 启动定时器
  setTimeout(interval, timeout);
  // 返回控制器
  return timer;
}

268、使用setInterval实现setTimeout

    function mySetInterval(fn, t) {
      const timer = setInterval(() => {
        clearInterval(timer)
        fn()
      }, t)
    }
 
    mySetInterval(() => {
      console.log('hoho');
    }, 1000)

269、提取出url 里的参数并转成对象

function getUrlParams(url){
  let reg = /([^?&=]+)=([^?&=]+)/g
  let obj = { }
  url.replace(reg, function(){
      obj[arguments[1]] = arguments[2]
  })
  // 或者
  const search = window.location.search
  search.replace(/([^&=?]+)=([^&]+)/g, (m, $1, $2)=>{obj[$1] = decodeURIComponent($2)})
  
  return obj
}
let url = 'https://www.junjin.cn?a=1&b=2'
console.log(getUrlParams(url)) // { a: 1, b: 2 }

270、将金额转换为千分位表示法

function thousandSeparator(num) {
  const str = num.toString();
  const result = str.replace(/(\d)(?=(?:\d{3})+(?!\d))/g, '$1,');
  
  return result;
}

// Example Usage:
console.log(thousandSeparator(1234567));         // Output: "1,234,567"
console.log(thousandSeparator(987654321.02));    // Output: "987,654,321.02"

271、一个嵌套对象,拍平对象,实现一个key对应一个简单类型的值

假设有一个嵌套对象如下所示:

const nestedObject = {
  key1: 'value1',
  key2: {
    key3: 'value2',
    key4: {
      key5: 'value3'
    }
  },
  key6: {
    key7: {
      key8: 'value4'
    }
  }
};

编写一个递归函数,该函数接收一个对象作为参数,并遍历该对象的每个属性:

function flattenObject(obj) {
  let result = {};

  function recurse(curr, prop) {
    if (Object(curr) !== curr) {
      result[prop] = curr;
    } else if (Array.isArray(curr)) {
      for (let i = 0, l = curr.length; i < l; i++) {
        recurse(curr[i], prop + '[' + i + ']');
      }
      if (l == 0) {
        result[prop] = [];
      }
    } else {
      let isEmpty = true;
      for (let p in curr) {
        isEmpty = false;
        recurse(curr[p], prop ? prop + '.' + p : p);
      }
      if (isEmpty && prop) {
        result[prop] = {};
      }
    }
  }

  recurse(obj, '');
  return result;
}

const flattenedObject = flattenObject(nestedObject);
console.log(flattenedObject);

展开后的结果:

展开后的对象 flattenedObject 将会是一个只包含简单类型值的对象,如下所示:

{
  'key1': 'value1',
  'key2.key3': 'value2',
  'key2.key4.key5': 'value3',
  'key6.key7.key8': 'value4'
}

通过以上递归展开对象的方法,可以将嵌套对象展平成一个只包含简单类型值的对象,便于后续处理和操作。

272、写一个函数,入参是一个类数组

写一个函数,入参是一个类数组,如果里面元素有promise(元素有可能不是promise),返回最后一个执行完的promise,如果全部reject,抛出异常

处理类数组中的 Promise 对象:

首先,我们需要遍历类数组中的每个元素,检查是否为 Promise 对象。如果是 Promise 对象,我们可以利用 Promise.allSettled 方法获取每个 Promise 的状态(fulfilled 或 rejected)。

返回最后一个执行完的 Promise:

然后,我们可以筛选出所有已执行完成的 Promise,并找到最后一个执行完成的 Promise。如果全部 Promise 都被 reject,则抛出一个异常。

代码实现如下:

function getLastFulfilledPromise(arr) {
  const promises = arr.filter(item => item instanceof Promise);

  if (promises.length === 0) {
    throw new Error('No Promise found in the input array');
  }

  return Promise.allSettled(promises)
    .then(results => {
      const fulfilledPromises = results.filter(result => result.status === 'fulfilled');

      if (fulfilledPromises.length === 0) {
        throw new Error('All Promises are rejected');
      }

      return fulfilledPromises[fulfilledPromises.length - 1].value;
    });
}

// 示例类数组
const arr = [
  Promise.resolve('First Promise'),
  'Not a Promise',
  Promise.reject(new Error('Rejected Promise')),
  Promise.resolve('Last Promise')
];

// 调用函数并处理结果
getLastFulfilledPromise(arr)
  .then(result => {
    console.log('Last fulfilled promise:', result);
  })
  .catch(error => {
    console.error('Error:', error.message);
  });

解释:

  • 函数 getLastFulfilledPromise 接受一个类数组作为参数,筛选出其中的 Promise 对象,并返回最后一个执行完成的 Promise。
  • 我们使用 Promise.allSettled 来等待所有 Promise 执行完成,并对执行结果进行处理。
  • 如果所有 Promise 都被 reject,则抛出一个异常。
  • 最后,处理返回的结果或捕获抛出的异常进行相应操作。

通过以上代码实现,可以满足题目要求,获取类数组中最后一个执行完的 Promise,同时处理异常情况。

273、反转字符串

反转字符串,输入 www.a.com.cn,输出cn.com.a.www

在JavaScript中,要实现一个反转字符串的方法,并且能够正确处理包含多个点(.)的域名,可以先将字符串按照点分割成数组,然后反转数组,最后再将数组元素连接起来,中间用点连接

274、手写实现Js中的new

(1)首先创建了一个新的空对象

(2)设置原型,将对象的原型设置为函数的 prototype 对象。

(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)

(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

 function myNew(fn, ...args) {
      // 判断参数是否是一个函数
      if (typeof fn !== "function") {
        return console.error("type error");
      }
      // 创建一个对象,并将对象的原型绑定到构造函数的原型上
      const obj = Object.create(fn.prototype);
      const value = fn.apply(obj, args); // 调用构造函数,并且this绑定到obj上
      // 如果构造函数有返回值,并且返回的是对象,就返回value ;否则返回obj
      return value instanceof Object ? value : obj;
}

275、手写实现Js中的call

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。

  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window 。

  3. 处理传入的参数,截取第一个参数后的所有参数。

  4. 将函数作为上下文对象的一个属性。

  5. 使用上下文对象来调用这个方法,并保存返回结果。

  6. 删除刚才新增的属性。

  7. 返回结果。

    // call函数实现
    Function.prototype.myCall = function(context) {
      // 判断调用对象
      if (typeof this !== "function") {
        console.error("type error");
      }
      // 获取参数
      let args = [...arguments].slice(1),
          result = null;
      // 判断 context 是否传入,如果未传入则设置为 window
      context = context || window;
      // 将调用函数设为对象的方法
      context.fn = this;
      // 调用函数
      result = context.fn(...args);
      // 将属性删除
      delete context.fn;
      return result;
    };
    

276、手写实现Js中的apply

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。

  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window 。

  3. 将函数作为上下文对象的一个属性。

  4. 判断参数值是否传入

  5. 使用上下文对象来调用这个方法,并保存返回结果。

  6. 删除刚才新增的属性

  7. 返回结果

    // apply 函数实现
    Function.prototype.myApply = function(context) {
      // 判断调用对象是否为函数
      if (typeof this !== "function") {
        throw new TypeError("Error");
      }
      let result = null;
      // 判断 context 是否存在,如果未传入则为 window
      context = context || window;
      // 将函数设为对象的方法
      context.fn = this;
      // 调用方法
      if (arguments[1]) {
        result = context.fn(...arguments[1]);
      } else {
        result = context.fn();
      }
      // 将属性删除
      delete context.fn;
      return result;
    };
    

277、手写实现Js中的bind

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。

  2. 保存当前函数的引用,获取其余传入参数值。

  3. 创建一个函数返回

  4. 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。

    // bind 函数实现
    Function.prototype.myBind = function(context) {
      // 判断调用对象是否为函数
      if (typeof this !== "function") {
        throw new TypeError("Error");
      }
      // 获取参数
      var args = [...arguments].slice(1),
          fn = this;
      return function Fn() {
        // 根据调用方式,传入不同绑定值
        return fn.apply(
          this instanceof Fn ? this : context,
          args.concat(...arguments)
        );
      };
    };
    

278、手写实现Js中的instanceof

instanceof 运算符用于测试构造函数的 prototype 属性是否出现在对象原型链中的任何位置。以下是手写实现 instanceof 的代码:

function myInstanceOf(obj, constructor) {
  // 获取 obj 的原型
  let proto = Object.getPrototypeOf(obj)
  while (proto != null) {
    // 判断原型是否是 constructor 的 prototype
    if (proto === constructor.prototype) {
      return true
    }
    proto = Object.getPrototypeOf(proto)
  }
  return false
}

// Example usage:
function Person(name) {
  this.name = name
}

const john = new Person('John')
console.log(myInstanceOf(john, Person)) // Output: true

上述代码中,我们定义了一个 myInstanceOf 函数,它接收两个参数:obj 表示待检测的对象,constructor 表示构造函数。该函数使用 Object.getPrototypeOf() 方法获取 obj 的原型,并逐层向上遍历原型链,判断每个原型是否等于 constructorprototype,如果找到则返回 true,否则返回 false

279、手写实现Js中的Object.create

Object.create() 是 JavaScript 中一种用于创建新对象的方法。该方法接受一个对象作为参数,并返回一个新对象,这个新对象的原型链指向参数对象。Object.create() 能够实现基于原型继承的面向对象编程。

以下是手写实现 Object.create() 的代码:

function create(obj) {
  function F() {}
  F.prototype = obj
  return new F()
}

// Example usage:
const person = {
  name: 'John',
  age: 30,
  sayHello() {
    console.log(`Hello, my name is ${this.name}`)
  }
}

const john = create(person)
john.sayHello() // Output: "Hello, my name is John"

上述代码中,我们定义了一个 create 函数,它接收一个对象作为参数 obj,并返回一个新的函数。该新函数使用一个空函数 F 作为构造函数,通过将构造函数的原型指向 obj 对象,实现了原型链继承。最后,将该新函数通过 new 操作符实例化成一个新的对象,该对象的原型链指向 obj 对象。

280、手写实现数组的map方法

map() 方法返回一个新数组,其元素由原始数组的每个元素调用回调函数处理而来。

Array.prototype.myMap = function (callback) {
  const result = [];
  
  for(let i = 0; i < this.length; i++) {
    result.push(callback(this[i], i, this));
  }
  
  return result;
}

// Example Usage:
const arr = [1, 2, 3];
const mappedArr = arr.myMap(x => x * 2);
console.log(mappedArr);   // Output: [2, 4, 6]

以上代码中,我们给 Array 对象的原型添加了一个 myMap() 方法,该方法接收一个回调函数 callback,它会依次遍历当前数组的每个元素,并将这些元素作为参数传递给回调函数进行处理,然后将得到的结果存储在一个新数组中返回。

281、手写实现数组的filter方法

filter() 方法用于筛选数组中符合条件的元素,并返回一个新数组。

Array.prototype.myFilter = function (callback) {
  const result = [];
  
  for(let i = 0; i < this.length; i++) {
    if(callback(this[i], i, this)) {
      result.push(this[i]);
    }
  }
  
  return result;
}

// Example Usage:
const arr = [1, 2, 3, 4, 5];
const filteredArr = arr.myFilter(x => x % 2 === 1);
console.log(filteredArr);   // Output: [1, 3, 5]

以上代码中,我们给 Array 对象的原型添加了一个 myFilter() 方法,该方法接收一个回调函数 callback,它会依次遍历当前数组的每个元素,并将这些元素作为参数传递给回调函数进行处理,如果回调函数返回值为 true,则将该元素加入到一个新数组中,并最终返回这个新数组。

282、手写实现数组的some方法

some() 方法用于检测数组中是否有元素满足指定条件,它会依次遍历数组中的每个元素,直到找到一个满足条件的元素,然后返回 true,否则返回 false

Array.prototype.mySome = function (callback) {
  for(let i = 0; i < this.length; i++) {
    if(callback(this[i], i, this)) {
      return true;
    }
  }
  return false;
}

// Example Usage:
const arr = [1, 2, 3, 4, 5]
const isEven = x => x % 2 === 0
console.log(arr.mySome(isEven)) // Output: true

以上代码中,我们给 Array 对象的原型添加了一个 mySome() 方法,该方法接收一个回调函数 callback,然后遍历整个数组,对于每个元素都执行回调函数,并判断回调函数的返回值是否为 true。如果存在元素使得回调函数的返回值为 true,则立即返回 true,表示数组中“有”元素满足条件;否则,当遍历完整个数组后,返回 false,表示没有任何元素满足条件。

283、手写实现数组的every方法

every() 方法用于检测数组所有元素是否都满足指定条件,它会依次遍历数组中的每个元素,若其中有任意一个元素不满足条件,则返回 false,否则返回 true

Array.prototype.myEvery = function (callback) {
  for(let i = 0; i < this.length; i++) {
    if(!callback(this[i], i, this)) {
      return false;
    }
  }
  return true;
}

// Example Usage:
const arr = [1, 2, 3, 4, 5]
const isEven = x => x % 2 === 0
console.log(arr.myEvery(isEven)) // Output: false

284、手写实现数组的find方法

find() 方法用于返回数组中第一个满足指定条件的元素的值,若不存在满足条件的元素则返回 undefined。它会依次遍历数组中的每个元素,找到第一个满足条件的元素后就停止遍历。

Array.prototype.myFind = function (callback) {
  for(let i = 0; i < this.length; i++) {
    if(callback(this[i], i, this)) {
      return this[i];
    }
  }
  return undefined;
}

// Example Usage:
const arr = [1, 2, 3, 4, 5]
const isEven = x => x % 2 === 0
console.log(arr.myFind(isEven)) // Output: 2

285、手写实现数组的forEach方法

forEach() 方法用于对数组中的每个元素执行指定操作,它不会返回任何值,只是调用回调函数进行某些操作。forEach() 方法不能中途停止或跳出循环。

Array.prototype.myForEach = function (callback) {
  for(let i = 0; i < this.length; i++) {
    callback(this[i], i, this);
  }
}

// Example Usage:
const arr = [1, 2, 3, 4, 5]
arr.myForEach(item => console.log(item))

// Output:
// 1
// 2
// 3
// 4
// 5

286、手写实现数组的reduce方法

reduce() 方法用于对数组中的所有元素执行指定的操作,并且返回一个累加的结果。它接收两个参数,第一个是回调函数 callback,第二个是初始值 initialValue。在每次回调函数执行时,当前值(或者初始值)会作为前一个值传递给回调函数,并将回调函数的返回值作为下一次执行的前一个值。最终,该方法会返回整个累加的结果。

Array.prototype.myReduce = function (callback, initialValue) {
  let accumulator = initialValue === undefined ? undefined : initialValue;
  
  for(let i = 0; i < this.length; i++) {
    if(accumulator !== undefined) {
      accumulator = callback.call(undefined, accumulator, this[i], i, this);
    } else {
      accumulator = this[i];
    }
  }
  
  return accumulator;
}

// Example Usage:
const arr = [1, 2, 3, 4, 5]
const sum = (prev, curr) => prev + curr;
console.log(arr.myReduce(sum)) // Output: 15

以上代码中,我们给 Array 对象的原型添加了一个 myReduce() 方法,该方法接收一个回调函数 callback 和一个可选的初始值 initialValue,然后遍历整个数组,对于每个元素都执行回调函数,并将当前值以及上一次执行回调函数得到的结果(或者初始值)作为参数传递进去,并将返回值作为下一次执行的前一个值。最终返回整个累加的结果。

287、手写实现数组的flat方法

flat() 方法用于将嵌套的数组展开成一维数组。它接收一个参数 depth,表示递归展开的深度。默认情况下,该方法只会展开一层,但是如果设置了深度参数,则会递归展开到指定的深度。

Array.prototype.myFlat = function (depth = 1) {
  const result = [];
  
  function flatten(arr, d) {
    arr.forEach(item => {
      if(Array.isArray(item) && d > 0) {
        flatten(item, d - 1);
      } else {
        result.push(item);
      }
    });
  }
  
  flatten(this, depth);
  
  return result;
}

// Example Usage:
const arr1 = [1, 2, 3, [4, 5]];
console.log(arr1.myFlat());     // Output: [1, 2, 3, 4, 5]
console.log(arr1.myFlat(1));   // Output: [1, 2, 3, 4, 5]
console.log(arr1.myFlat(2));   // Output: [1, 2, 3, 4, 5]

const arr2 = [1, [2, [3, [4]], 5]];
console.log(arr2.myFlat());     // Output: [1, 2, [3, [4]], 5]
console.log(arr2.myFlat(1));   // Output: [1, 2, [3, [4]], 5]
console.log(arr2.myFlat(2));   // Output: [1, 2, 3, [4], 5]
console.log(arr2.myFlat(3));   // Output: [1, 2, 3, 4, 5]

288、手写实现数组的push方法

push() 方法用于在数组的末尾添加一个或多个元素,并返回修改后数组的新长度。

Array.prototype.myPush = function (...items) {
  let length = this.length;
  
  for(let i = 0; i < items.length; i++) {
    this[length++] = items[i];
  }
  
  return length;
}

// Example Usage:
const arr = [1, 2, 3];
console.log(arr.myPush(4));       // Output: 4
console.log(arr);                 // Output: [1, 2, 3, 4]

console.log(arr.myPush(5, 6, 7)); // Output: 7
console.log(arr);                 // Output: [1, 2, 3, 4, 5, 6, 7]

289、微任务和宏任务

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

290、(a == 1 && a == 2 && a == 3) 有可能是 true 吗?

方案一:重写toString()或valueOf()

let a = {  
    i: 1,  
    toString: function () {    
        return a.i++;  
    }
}
console.log(a == 1 && a == 2 && a == 3); // true

方案二:数组

数组的toString接口默认调用数组的join方法,重写join方法。定义a为数字,每次比较时就会调用 toString()方法,我们把数组的shift方法覆盖toString即可:

let a = [1,2,3];
a.toString = a.shift;
console.log(a == 1 && a == 2 && a == 3); // true

当然把toString改为valueOf也是一样效果:

let a = [1,2,3];
a. valueOf  = a.shift;
console.log(a == 1 && a == 2 && a == 3); // true

方案三:使用Object.defineProperty()

Object.defineProperty()用于定义对象中的属性,接收三个参数:object对象、对象中的属性,属性描述符。属性描述符中get:访问该属性时自动调用。

var  _a = 1;
Object.defineProperty(this,'a',{  
    get:function(){    
        return _a++  
    }
})
console.log(a===1 && a===2 && a===3)//true

291、Ts中内置的工具类型

TypeScript 中内置了一些常用的工具类型:

  1. Partial:将类型 T 中所有属性转换为可选属性。
  2. Required:将类型 T 中所有属性转换为必选属性。
  3. Readonly:将类型 T 中所有属性转换为只读属性。
  4. Record<K, T>:定义一个属性名为 K 类型,属性值为 T 类型的对象类型。
  5. Pick<T, K>:从类型 T 中选择属性名为 K 的属性类型组成一个新类型。
  6. Omit<T, K>:从类型 T 中剔除属性名为 K 的属性类型组成一个新类型。
  7. Exclude<T, U>:从类型 T 中剔除可以赋值给类型 U 的类型组成一个新类型。
  8. Extract<T, U>:从类型 T 中提取可以赋值给类型 U 的类型组成一个新类型。
  9. NonNullable:从类型 T 中剔除 null 和 undefined 类型组成一个新类型。
  10. ReturnType:获取函数类型 T 返回值类型。

技术详解

Partial 工具类型可以将一个对象的所有属性都变为可选(Optional)

interface User {
  name: string;
  age: number;
  email: string;
}

type PartialUser = Partial<User>;

/*
PartialUser 类型为:
{
  name?: string | undefined;
  age?: number | undefined;
  email?: string | undefined;
}
*/

Required 工具类型可以将一个对象的所有属性都变为必需(Required)

interface User {
  name?: string;
  age?: number;
  email?: string;
}

type RequiredUser = Required<User>;

/*
RequiredUser 类型为:
{
  name: string;
  age: number;
  email: string;
}
*/

Pick 工具类型可以从一个对象中选择指定的属性,并返回一个新的对象类型

interface User {
  name: string;
  age: number;
  email: string;
}

type UserBasicInfo = Pick<User, 'name' | 'email'>;

/*
UserBasicInfo 类型为:
{
  name: string;
  email: string;
}
*/

Omit 工具类型可以从一个对象中省略指定的属性,并返回一个新的对象类型

interface User {
  name: string;
  age: number;
  email: string;
}

type UserWithoutAge = Omit<User, 'age'>;

/*
UserWithoutAge 类型为:
{
  name: string;
  email: string;
}
*/

Record 工具类型可以根据指定的键值对生成一个新的对象类型

type Fruit = 'apple' | 'banana' | 'orange';

type Inventory = Record<Fruit, number>;

/*
Inventory 类型为:
{
  apple: number;
  banana: number;
  orange: number;
}
*/

在上面的代码中,我们定义了一个 Fruit 类型,表示水果的种类,并使用 Record 工具类型根据 Fruit 类型生成了一个新的对象类型 Inventory。该类型的键为 Fruit 类型中的每个元素,值为 number 类型。这样,我们就可以快速生成一个具有一组特定键值对的对象类型。

Exclude 工具类型可以从一个联合类型中排除某些类型

type Platform = 'Windows' | 'MacOS' | 'Linux' | 'iOS' | 'Android';

type DesktopPlatform = Exclude<Platform, 'iOS' | 'Android'>;

// DesktopPlatform 类型为:'Windows' | 'MacOS' | 'Linux'

NonNullable 工具类型可以将一个类型中的 null 和 undefined 类型排除

type NullableString = string | null | undefined;

type NonNullableString = NonNullable<NullableString>;

// NonNullableString 类型为:string

292、Ts中常用的自定义工具类型

  1. DeepPartial:将类型 T 中所有嵌套对象的属性转换为可选属性。
  2. DeepRequired:将类型 T 中所有嵌套对象的属性转换为必选属性。
  3. DeepReadonly:将类型 T 中所有嵌套对象的属性转换为只读属性。
  4. UnionToIntersection:将联合类型 T 中所有成员的属性类型进行交叉操作,得到一个新类型。
  5. Diff<T, U>:从类型 T 中排除所有类型 U 中也包含的类型。
  6. Intersect<T, U>:获取类型 T 和类型 U 中共同拥有的属性所组成的新类型。

293、Ts中type和interface的区别

  • typeinterface 都是用来定义类型的关键字,都可以用来定义对象、函数、类等等类型
  • type可以定义更多类型:使用type关键字可以定义很多interface不支持的类型,比如联合类型、交叉类型、元组类型、类型别名等等。
  • interface可以被合并:当定义相同名称的interface时,它们会自动合并为一个类型。使用type定义的类型则不支持合并,如果重复定义同一个名称的type,就会报错。
  • interface支持扩展:使用extends关键字可以让一个interface继承另一个interface,从而实现接口的扩展。当然,使用type也可以通过交叉类型来实现类型的扩展,但相对来说比较繁琐。

294、Ts中的any、unkonwn、never

  1. any类型:是一种不安全的类型,它可以表示任何类型的值。
  2. unknown类型:表示值的类型可能是任何类型,但我们目前并不知道它的类型。
  3. never类型:表示永远不会发生的值,是其它类型的子类型。

295、Ts中的泛型

  • 如果说 TypeScript 是一门对类型进行编程的语言,那么泛型就是这门语言里的(函数)参数。即是一种在设计时不指定具体类型,而在使用时再指定类型的技术。
  • 通过使用泛型,可以创建可重用的组件,这些组件能够支持多种类型,提高代码的可读性和可维护性。
  • 比如,可以定义一个名为identity的泛型函数,该函数接收一个类型为 T 的参数,返回值也为类型为 T 的值。

296、TS中怎么给引入的第三方库设置类型声明文件

  1. 使用 @types 安装对应的类型声明包
  2. 手动编写类型声明文件
  3. 使用 tsconfig.json 中的 typeRoots 和 types 选项

技术详解

使用 **@types** 安装对应的类型声明包

可以使用 npmyarn 安装名为 @types/[package-name] 的类型声明包,例如安装 @types/react 包来为 React 库添加类型支持。这样就不需要手动创建类型声明文件了,类型声明会自动加载。

手动编写类型声明文件

手动编写类型声明文件也是一种为第三方库添加类型支持的方式。可以创建一个名为 index.d.ts 的文件,将它放在该库的根目录下,然后将类型声明代码放在这个文件中,最后将这个文件放在项目中的某个位置即可。

使用 tsconfig.json 中的 typeRoots 和 types 选项

在 tsconfig.json 文件中的 typeRootstypes 选项可以用来配置 type declaration 文件的搜索路径和要添加到编译上下文中的类型声明文件。其中,typeRoots 是一个数组,用来指定 TypeScript 应该搜索类型声明文件的目录,而 types 是一个数组,用来指定应该加入编译上下文中的类型声明文件。

比如,要为 lodash 添加类型支持,可以在 tsconfig.json 中进行如下配置:

{
  "compilerOptions": {
    "typeRoots": ["node_modules/@types"],
    "types": ["lodash"]
  }
}

这样 TypeScript 就会在 node_modules/@types 目录下搜索类型声明文件,并将 lodash 的类型声明文件加入编译上下文中。

297、说说对 TypeScript 装饰器的理解

TypeScript 装饰器是一种特殊的声明,它可以用来装饰类、方法、属性或者参数。用于动态地修改、增强或注释这些成员。

比如,一些 TypeScript 中常用的装饰器:

  1. @classDecorator: 用于装饰类,可以在类声明之前使用。它接收一个参数,表示被装饰的类构造函数。

  2. @propertyDecorator: 用于装饰类的属性,可以在属性声明之前使用。它接收两个参数,分别表示被装饰的属性名称和其所属的类构造函数。

  3. @methodDecorator: 用于装饰类的方法,可以在方法声明之前使用。它接收三个参数,分别表示被装饰的方法名称、方法的属性描述符和其所属的类构造函数。

  4. @parameterDecorator: 用于装饰方法的参数,可以在方法参数声明之前使用。它接收三个参数,分别表示被装饰的参数名称、参数的属性描述符和其所属的方法。

  5. @factoryDecorator: 用于装饰工厂方法,可以在工厂方法的返回值声明之前使用。它接收一个参数,表示被装饰的工厂方法。

  6. @getterSetterDecorator: 用于装饰类的 getter 和 setter 方法,可以在方法声明之前使用。它接收三个参数,分别表示被装饰的方法名称、方法的属性描述符和其所属的类构造函数。

TypeScript 装饰器是一种特殊的声明,它可以用来装饰类、方法、属性或者参数。它们可以看作是将一个函数挂载到一个类、方法、属性或参数上,用于动态地修改、增强或注释这些成员。

装饰器通过 @ 符号来表示,放在被装饰的成员上面,可以接收一个或多个参数。装饰器可以用来实现各种功能,比如实现 AOP(面向切面编程)、添加元数据、验证输入参数合法性等。

TypeScript 内置了几个装饰器,比如 @deprecated 表示该成员已经过时,不建议使用;@abstract 表示该成员是抽象成员,需要在子类中进行实现。开发者也可以自定义装饰器来满足自己的需求。

装饰器是 TypeScript 的一个重要特性,可以大幅提高代码的灵活性和可读性,对于大型项目特别有用。

298、BFC

BFC(Block Formatting Context)是 CSS 中一个很重要的概念。它是指一个块级容器,其中的元素按照特定规则布局和渲染,同时也影响着其内部和外部元素的布局。

BFC 特点:

  1. BFC 内部的元素会按照垂直方向一个接一个地排列,并且在水平方向上占据整个父容器的宽度。

  2. BFC 内部元素的 margin 和 padding 不会与外部元素共享边框,而是互相独立,不会发生重叠。

  3. 如果两个相邻的块级元素都属于同一个 BFC,那么它们之间的 margin 会产生折叠,即取两个 margin 的最大值作为最终的 margin 值。

  4. BFC 可以包含浮动元素,并防止浮动元素溢出到容器外面。

  5. BFC 内部的第一个子元素或最后一个子元素,可以通过设置 clear 属性来清除浮动。

如何创建BFC?

  1. 使用 float 属性:给元素添加 float 属性可以使其成为一个 BFC。

  2. 使用 position 属性:将元素使用 position 属性设置为 absolute 或 fixed 时,也可以使其成为一个 BFC。

  3. 使用 display 属性:给元素添加 display 属性设置为 inline-block、table-cell、table-caption 等值,也可以变成 BFC。

  4. 设置 overflow 属性:将元素的 overflow 属性设置为 auto、scroll 或 hidden,也可以创建一个 BFC。

应用场景:

  1. 清除浮动:当一个父容器包含多个浮动元素时,可以将其设置为 BFC,防止浮动元素溢出到外面。

  2. 解决 margin 重叠问题:当两个元素的 margin 发生重叠时,可以将其中之一包裹在一个 BFC 中,使其 margin 与外部元素分离。

  3. 实现多列布局:使用 column-count 和 column-gap 属性可以让文本内容自动分为多列,但这需要在 BFC 中实现。

总结

BFC 是 CSS 中的一个重要概念,它对于页面布局及解决一些常见问题非常有帮助。了解 BFC 的概念、特点和创建方式,能够更好地掌握其应用场景,提高开发效率和代码质量。

299、Css中如何减少回流、重绘

  • 回流(重排)指的是当页面中的元素发生布局或几何属性发生变化时,浏览器需要重新计算这些元素的位置和大小,然后重新构建页面的渲染树,这个过程称为回流。由于需要重新计算布局,回流的代价很大,会对页面的性能产生负面影响。
  • 重绘指的是当页面中的元素样式发生改变时,浏览器会重新绘制这些元素的外观,但不会改变它们在页面中的位置和大小。重绘的代价相对较小,但仍然会对页面性能产生一定的影响。

减少回流和重绘是优化 Web 页面性能中非常重要的一环,可以提高页面的加载速度和用户体验。下面列出一些常见的实践:

  1. 避免频繁操作样式属性:在 JavaScript 中频繁地修改样式属性值会导致回流和重绘。因此,可以使用 CSS 类来设置多个属性,通过添加或删除类名来改变元素的样式。

  2. 将样式集中到单个类中:许多元素具有相同的样式,因此将它们放在同一个类中可以减少样式的数量,并避免不必要的回流和重绘。

  3. 使用 transform 替代 top/left:通过使用 transform 属性来移动元素,而不是使用 top 和 left 属性,可以减少重绘和回流次数。

  4. 使用 position: absolute 替代 float:在布局时,使用 position: absolute 属性可以避免使用 float 带来的副作用,如重绘和回流。

  5. 避免设置大量的 DOM 元素:页面上 DOM 元素的数量越多,回流和重绘的次数就越多。如果可能的话,可以尝试将多个元素合并成一个。

  6. 隐藏元素而不是删除它们:当需要动态更新页面内容时,不要使用 display: none 或者 removeChild 来删除元素,这会导致重绘和回流。相反,可以使用 visibility: hidden 或者将元素移出可视区域。

  7. 使用 transform 代替改变元素的位置和大小、将需要多次操作的元素缓存为变量、避免循环中不必要的 DOM 操作等。

300、documentFragment (文档碎片)是什么

DocumentFragment(文档碎片)是一个DOM节点类型,它表示一个轻量级的文档对象,它可以包含和操作其他DOM节点,但不会像普通DOM节点那样被直接渲染到页面上。
因此,使用DocumentFragment可以在内存中操作DOM节点,而不会引起重绘或回流等性能问题。

与直接操作DOM的区别在于,我们可以将需要插入到页面上的DOM节点,先插入到DocumentFragment中,然后再一次性地将其插入到页面中。

这样做的好处是可以减少DOM操作次数,从而提高页面性能。特别是在需要频繁地添加、删除多个DOM节点时,使用DocumentFragment可以显著提升页面性能。

下面是使用DocumentFragment的示例代码:

// 创建一个documentFragment对象
var frag = document.createDocumentFragment();

// 创建多个li元素并添加到documentFragment中
for (var i = 0; i < 10; i++) {
  var li = document.createElement('li');
  li.innerText = 'List item ' + i;
  frag.appendChild(li);
}

// 将documentFragment一次性添加到ul中
document.querySelector('ul').appendChild(frag);

上述代码中,我们首先创建了一个DocumentFragment对象,然后使用循环创建多个li元素,并将它们添加到DocumentFragment中。最后,我们一次性将DocumentFragment对象添加到页面上的ul元素中。

301、Css中移动端适配有哪些方案

  1. 首先,通过meta标签设置viewport
  2. rem单位搭配@media媒体查询:可以通过使用rem单位,它以HTML元素的font-size为比例,也可以搭配 postcss-pxtorem 搭建项目
  3. vw/vh 布局:也可以通过使用vw/vh 布局,vw/vh 方案与 rem 方案类似,都是将页面分成一份一份的,只不过 vw/vh 是将页面分为 100 份,也可以搭配 postcss-px-to-viewport 搭建项目
  4. 百分比布局:也可以使用百分比来实现布局,但是需要特定宽度时,这个百分比的计算对开发者来说并不友好,且元素百分比参考的对象为父元素,元素嵌套较深时会有问题。

302、Css中怎么解决浏览器兼容问题

  1. 使用特定浏览器的CSS前缀:不同的浏览器可能会对某些CSS属性有不同的实现方式,为了解决这类问题,我们可以使用特定浏览器的CSS前缀。例如,-webkit-、-moz-、-o-、-ms- 分别代表 Chrome/Safari、Firefox、Opera、IE 浏览器。在 Webpack 中,可以使用 postcss-loaderautoprefixer 插件来给 CSS 属性加前缀。

  2. 使用 CSS reset 或 normalize.css:由于不同的浏览器对默认样式的实现可能不同,如果我们不采取措施,就有可能导致页面在不同浏览器上显示效果不同。因此,使用 CSS reset 或 normalize.css 可以帮助我们重置浏览器的默认样式,并统一不同浏览器之间的差异。

在 Webpack 中,可以使用 postcss-loaderautoprefixer 插件来给 CSS 属性加前缀。

postcss-loader 是一个基于 PostCSS 的 Webpack Loader,它可以对 CSS 进行转换和优化。而 autoprefixer 则是一个用于自动添加浏览器前缀的 PostCSS 插件,它可以根据配置的浏览器兼容性信息为 CSS 属性自动添加前缀。

使用 postcss-loaderautoprefixer 的过程如下:

安装 postcss-loaderautoprefixer 插件:

npm install postcss-loader autoprefixer --save-dev

在 Webpack 配置文件中添加 postcss-loader

module.exports = {
  // ...
  module: {
    rules: [
      // ...
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              plugins: [
                require('autoprefixer')
              ]
            }
          }
        ]
      }
      // ...
    ]
  }
}

以上配置会将 CSS 文件交给 postcss-loader 处理,在 plugins 选项中指定要使用的 PostCSS 插件,这里只使用了 autoprefixer。在 autoprefixer 插件的参数中,可以指定需要兼容的浏览器版本,例如:

{
  loader: 'postcss-loader',
  options: {
    plugins: [
      require('autoprefixer')({
        // 指定为最近的两个版本和 IE9 及以上版本
        browsers: ['last 2 versions', 'ie >= 9']
      })
    ]
  }
}

使用 postcss-loaderautoprefixer 可以让我们在编写 CSS 样式时,无需手动为各个浏览器添加前缀,从而提高开发效率。

303、画一条0.5px的线

  • border-image 属性可以通过设置图片来实现边框的绘制,这样即使缩放,图片仍然能够保持高清晰度

    div { border: none; border-image: url(border.png) 2 2 stretch; }

  • 一种方法是使用CSS3中的transform属性,将线条缩小一半

    hr {
      height: 1px;
      border: none;
      background-color: black;
      transform: scaleY(0.5);
    }
    
  • 另一种方法是使用伪元素和box-shadow属性来创建一条近似0.5像素的线

    hr {
      height: 1px;
      border: none;
      position: relative;
    }
    hr::after {
      content: "";
      display: block;
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 1px;
      box-shadow: 0 0.25px 0.25px rgba(0, 0, 0, 0.5);
    }
    

304、Css中如何优化动画

  1. 使用transform和opacity属性:使用transform属性来移动、旋转或缩放元素,而不是使用top、left、right和bottom属性。使用opacity属性来控制元素的透明度。这样可以避免使用position属性和margin属性,从而提高动画性能。

  2. 使用will-change属性:使用will-change属性来告诉浏览器哪些属性将在未来的动画中发生变化。这样可以让浏览器提前优化这些属性,从而提高动画性能。

  3. 使用requestAnimationFrame函数:使用requestAnimationFrame函数来执行动画,而不是使用setTimeout或setInterval函数。requestAnimationFrame函数可以让浏览器在下一次重绘之前执行动画,从而提高动画性能。

  4. 使用硬件加速:使用transform和opacity属性可以触发硬件加速,从而提高动画性能。但是,需要注意的是,过多地使用硬件加速可能会导致性能问题。

  5. 避免使用过多的动画:避免使用过多的动画,尤其是在移动设备上。过多的动画可能会导致性能问题,从而影响用户体验。

  6. 使用CSS动画库:使用CSS动画库可以简化动画的编写过程,并提供更好的性能和兼容性。常见的CSS动画库包括Animate.css、GreenSock和Velocity.js等。

总之,优化动画的关键是减少浏览器的重绘和重排次数,尽可能地使用硬件加速,并避免使用过多的动画。

305、Css中的标准盒子模型和怪异盒子模型

盒子模型是指 HTML 中的元素在渲染成页面上的可视元素时,所呈现的一个矩形框,包括元素内容、内边距、边框和外边距等部分。CSS 中的盒子模型规定了这些部分相对于元素框的位置和大小。

CSS 中的 标准盒模型(也称 W3C 盒模型) 包括以下四个部分:

  1. 元素内容(Content):即元素中包含的文本或图像等具体内容。
  2. 内边距(Padding):位于元素内容和边框之间,用于控制元素内容与元素边框的间距。
  3. 边框(Border):围绕元素内容和内边距的一条线,用于控制元素的边界形状、宽度和颜色等。
  4. 外边距(Margin):位于元素边框和相邻元素之间,用于控制元素与相邻元素之间的距离。

怪异盒模型(也称 IE 盒模型) 的内边距和边框都会计入元素的宽度和高度中。也就是说,元素的宽度和高度包括内容、内边距和边框三个部分,而不是只包括内容部分。因此,当使用怪异盒模型时,需要在计算元素宽高时特别注意。

box-sizing 属性描述的是元素的盒模型类型,其默认值为 content-box,表示标准盒模型。我们可以将其设置为 border-box,以使用怪异盒模型。

具体地,box-sizing 属性可以设置以下三个值:

  1. content-box:默认值,表示标准盒模型,元素的宽度和高度只包括内容部分的宽度和高度。
  2. border-box:表示怪异盒模型,元素的宽度和高度包括内容、内边距和边框三个部分的宽度和高度。
  3. padding-box:元素的宽度和高度包括内容和内边距两个部分的宽度和高度,但不包括边框部分。

在实际开发中,使用 box-sizing 属性可以方便我们计算元素的宽高,特别是当需要控制元素的边框和内边距时,使用 border-box 不仅可以简化计算,还可以避免出现一些不必要的布局问题。