Skip to content

场景:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯不断交替重复亮灯?

javascript
// 第一种res不加括号,是因为直接将resolve函数作为参数传递给另一个函数,
// ms秒过后setTimeout自动调用执行
const sleep = s => new Promise(res => setTimeout(res, s * 1000))

// 下面这种,需要加括号调用,我们需要显示的执行res()函数,来解决这个Promise
const sleep = s => new Promise(res => setTimeout(() => res(), s * 10
javascript
const sleep = (s) => new Promise((res) => setTimeout(res, s * 1000))
sleep(3)
    .then(() => {
        console.log('红灯')
        return sleep(1)
    })
    .then(() => {
        console.log('绿灯')
        return sleep(2)
    })
    .then(() => {
        console.log('黄灯')
    })

// 改async await
async function trafficLight() {
    await sleep(1)
    console.log('红灯')
    await sleep(2)
    console.log('绿灯')
    await sleep(3)
    console.log('黄灯')
}
trafficLight()

// 改为自执行函数
;(async () => {
    await sleep(1)
    console.log('红灯')
    await sleep(2)
    console.log('绿灯')
    await sleep(3)
    console.log('黄灯')
})()
// 怎么让它执行的交替执行呢?
;(async () => {
    while (true) {
        await sleep(1)
        console.log('红灯')
        await sleep(2)
        console.log('绿灯')
        await sleep(3)
        console.log('黄灯')
    }
})()
javascript
// 红绿灯函数
function red() {
  console.log("red");
}
function green() {
  console.log("green");
}
function yellow() {
  console.log("yellow");
}

// 改sleep函数,等待时间后执行cb
const light = (cb,s) => new Promise(res => setTimeout(() => {
cb(),res()}, s * 1000)

编写一个TrafficLightController类,实现红绿灯状态切换,并在控制台打印输出

javascript
1. start()方法,实现红绿灯切换,绿灯:30s,黄灯3s,红灯20s
2. changeDuration()方法,可以改变红绿灯持续时间
3. stop()方法,停止切换
4. 可以使用Promise、async、await
javascript
class TrafficLightController {
    constructor() {
        this.greenDuration = 3
        this.yellowDuration = 1
        this.redDuration = 2
        this.isRunning = false
    }
    // start()方法,实现红绿灯切换,绿灯:3s,黄灯1s,红灯2s
    async start() {
        this.isRunning = true
        this.runLoop()
    }
    async runLoop() {
        while (true) {
            // 需要再每个await前面加判断,必须每个await前都加!!!
            if (!this.isRunning) break
            console.log(`绿灯:${this.greenDuration}s`)
            await this.delay(this.greenDuration)
          
            if (!this.isRunning) break
            console.log(`黄灯:${this.yellowDuration}s`)
            await this.delay(this.yellowDuration)
          
            if (!this.isRunning) break
            console.log(`红灯:${this.redDuration}s`)
            await this.delay(this.redDuration)
        }
    }
    delay(duration) {
        return new Promise((res) => {
            setTimeout(res, duration * 1000)
        })
    }
    // changeDuration()方法,可以改变红绿灯持续时间
    changeDuration(duration, color) {
        switch (color) {
            case 'red':
                this.redDuration = duration
            case 'green':
                this.greenDuration = duration
            case 'yellow':
                this.yellowDuration = duration
        }
    }
    stop() {
        this.isRunning = false
        console.log('Traffic light stopped.')
    }
}

const trafficLight = new TrafficLightController()

trafficLight.start()

setTimeout(() => {
    trafficLight.changeDuration(2, 'yellow')
}, 5 * 1000)
setTimeout(() => {
    trafficLight.stop()
}, 7 * 1000)

防抖节流手写

防抖 debounce

debounce

当持续的触发某个事件的时候,一定时间内没有再触发事件,回调函数才会执行一次。 如果我们的等待时间到来之前,又触发了这个事件,重新计时

javascript
// 防抖:防止抖动, 你先抖动着, 啥时候停了,再执行下一步
const debounce = (fn, ms = 0) => {
    let timerId 
    return function(...args){ // 剩余参数,真数组
        // console.log(x, y, z)
        clearTimeout(timerId)
        // 1. setTimeout外 this指向的this input绑定事件的元素
        timerId = setTimeout(() => {
            // 2. setTimeout内,如果改成箭头函数了,this也指向input
            fn.call(this, ...args)
        }, ms)
    }
}

节流

throttle

持续触发的事件,在一段事件内只允许函数执行一次

javascript
// 节流 减少事件执行的频率

// 定时器的写法
const throttle = (fn, ms = 0) => {
    let timerId 
    return function(...args){
        // 保证当前只有一个定时器,如果触发事件,不要执行后面代码
        if (timerId) return 
        timerId = setTimeout(() => {
            fn.apply(this, args)
            // 什么时候让可以重新开启定时器
            timerId = null
        })
    }
} 

// 节流, 时间戳的写法
const throttle2 = (fn, ms = 0) => {
    let start = 0
    return function(...args){
        // 判断当前时间 - 开始时间 >= ms ,就执行一次函数
        let now = Date.now()
        if (now - start >= ms) {
            fn.call(this, ...args)
            start = Date.now()
        }
    }
}

总结

https://www.30secondsofcode.org/js/s/debounce 目前看见最简写法,best!

防抖和节流的区别

  1. 防抖:减少执行次数,多次密集的触发只执行一次
  2. 节流:减少执行频率,有节奏的执行
  3. 防抖关注结果,节流关注过程
javascript
// 面试 ? 什么是防抖 什么是节流, 有实际使用过吗?自己封装过防抖或者节流函数吗?

// 1. 
// 防抖 
// 你先抖动着,什么时候停了,我再执行 
//  ==> 将多次密集的执行,合并为一次

// 节流
// => 减少事件触发的频率

const sendMsg = function(){
    console.log('发送请求')
}


// 2. 应用场景
// 防抖的应用场景: 搜索框,不断的输入文字,如果每次输入一个文字就发一个请求,相当消耗性能

// 节流的应用场景:
        // 1. scroll滚动事件
        // 2. mousemove鼠标移动事件
        // 3. 窗口的resize事件等等

// 3. 有没有自己封装或者写过? ==> 好一点的公司会问,大厂才会手写

const debounce = (fn, delay) => {
    // 1. return外面 ,立即执行,只执行一次
    let timerId
    return function(...args){
        // 2. return里面,每次触发就执行
        clearTimeout(timerId)
        // 这里的this指向input
        timerId = setTimeout(() => {
            console.log(this)
            fn.call(this, ...args)
        })
    }
}

// 4. let timerId 为什么要放到return的外面, 如果放到里面
// 5. 为什么改成箭头函数了?箭头函数没有this,箭头函数中的this,就是function里面的this

// 6. bind后面传的参数,给了谁?  fn.bind(obj, 1, 2, 3)  ==> 给了return后面的哪个function
// 7. 接收参数的方式,1. 剩余参数, 2. arguments
        // 7.1 如果用rest剩余参数接收 (...args)  ==> 内部如果使用call改变this指向
        // 需要把args这个真数组,展开成参数列表
       
        // 7.2 如果使用arguments来接收参数, 那么也是可以的,注意arguments是一个伪数组
        const debounce2 = (fn, delay) => {
            let timerId
            return function(){
                clearTimeout(timerId)
                timerId = setTimeout(() => {
                    console.log(this)
                    fn.call(this, ...arguments)
                })
            }
        }

// 8. 改变this指向,还可以使用apply改变
const debounce3 = (fn, delay) => {
            let timerId
            return function(...args){
                clearTimeout(timerId)
                timerId = setTimeout(() => {
                    fn.apply(this, args)
                })
            }
        }

const debounce4 = (fn, delay) => {
    let timerId
    return function(){
        clearTimeout(timerId)
        timerId = setTimeout(() => {
            fn.apply(this, arguments)
        })
    }
}



// 防抖,如果不改变this,不传参数,就这么调用,就欧克了
input.addEventListener('keyup', debounce(sendMsg, 300))
// 防抖,如果要改变this,并且传参数,使用bind来调用,改变this指向,并且传参数,
//  ===> 为什么使用bind,bind不是立即执行的,而是返回一个函数
input.addEventListener('keyup', debounce(sendMsg, 300).bind(input, 1, 2))

手写实现深拷贝

javascript
// 检测是否是对象
const isObject = (obj) => {
  return obj !== null && typeof obj === "object";
};

const deepClone = (obj, map = new WeakMap()) => {
  // 1. 基本数据类型,直接返回
  if (isObject(obj)) return obj;
  // 2. 处理日期对象
  if (obj instanceof Date) return new Date(obj);
  // 3. 处理正则
  if (obj instanceof RegExp) return new RegExp(obj);
  // 4. 解决循环引用问题,如果已经拷贝过当前对象,直接返回
  if (map.has(obj)) return map.get(obj);

  // 5. 拷贝逻辑 创建一个新的对象或者数组
  let clone = Array.isArray(obj) ? [] : {};
  // 6. 将新对象clone存储到map中,避免循环引用,obj为key
  map.set(obj, clone);

  // 7. 递归拷贝
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key], map);
    }
  }
  // 8.第7部分改为这个也ok
  // Reflect.ownKeys(obj).forEach((key) => {
  //   clone[key] = deepClone(obj[key], map);
  // });
  return clone;
};

// 为什么使用WeakMap?
// 因为WeakMap的键是弱引用,如果没有强引用指向这个键,
// 它对应的键值对就会自动的被垃圾回收机制清楚,不会造成内存泄露
const obj = {name:'xx', add:{x:100}}
const res = deepClone(obj)
obj.add.x = 200
console.log(res)

WeakMap 与 Map区别 有两点

  1. WeakMap只接受 对象和 Symbol值 作为键名
  2. WeakMap键名所指向的对象,不计入垃圾回收机制

基本上,如果要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。

冒泡排序

javascript
function bubbleSort(arr) {
  // 外层循环控制每次排序的轮数
  for (let i = 0; i < arr.length - 1; i++) {
    // 内层循环控制每次排序的比较和交换
    for (let j = 0; j < arr.length - 1 - i; j++) {
      // 如果前一个元素大于后一个元素,则交换它们的位置
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }
    }
  }
  return arr;
}

// 示例
const arr = [3, 1, 6, 2, 8, 5];
console.log(bubbleSort(arr)); // 输出 [1, 2, 3, 5, 6, 8]

实现Ajax

GET 请求

javascript
// 原生ajax 五步走
function ajax_get(){
    // 1. 实例化xhr对象  XMLHttpRequest 内置的
    const xhr = new XMLHttpRequest()
    // 2. 设置请求方法和地址 
    xhr.open('get', 'http://ajax-api.itheima.net/api/news')
    // 3. 设置请求头 ==> 可以省略 如果后端需要,就要设置,不需要,设置了也没关系,可以自定义
    xhr.setRequestHeader('myHeader', 'goodHeader')
    // 4. 注册回调函数  
    //  readystatechange  ==> 表示ajax请求的状态改变
    // xhr.readystate ==> 从我们实例化xhr对象开始,就在变化 0-4 值, 4 表示服务器已经把数据返回给我了
    xhr.addEventListener('readystatechange', function(){
        if (xhr.readyState === 4){

            // 什么时候是成功的请求状态 http状态?200 - 300   304 资源未修改,缓存
            if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304){
                console.log(xhr.response)
            } else {
                console.log('请求不成功')
            }

        }
    })

    // 5. 发送请求
    xhr.send()
}

POST 请求

javascript
// 原生ajax 五步走
function ajax_post_json(url, data){
    // 1. 实例化xhr对象  XMLHttpRequest 内置的
    const xhr = new XMLHttpRequest()
    // 2. 设置请求方法和地址 
    xhr.open('POST', url)
    // 3. 设置请求头 ==> 可以省略 如果后端需要,就要设置,不需要,设置了也没关系,可以自定义
    xhr.setRequestHeader('content-type', 'application/json')

    // 4. 注册回调函数  
    //  readystatechange  ==> 表示ajax请求的状态改变
    // xhr.readystate ==> 从我们实例化xhr对象开始,就在变化 0-4 值, 4 表示服务器已经把数据返回给我了
    xhr.addEventListener('readystatechange', function(){
 
        if (xhr.readyState !==4) return
        // 什么时候是成功的请求状态 http状态?200 - 300   304 资源未修改,缓存
        if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304){
            console.log(JSON.parse(xhr.response))
        } else {
            console.log('请求不成功')
        }

    })

    // 5. 发送请求
    // 如果是get请求,send可以为空,或者null
    // post请求,在这里写参数
    xhr.send(JSON.stringify(data))
}

fetch

javascript
// fetch 是一个现代的API,是基于XMLHttpRequest的嘛?不是的
// fetch 是基于promise实现的, promise 承诺 

const api = 'http://ajax-api.itheima.net/api/robot?spoken=我很好,哈哈哈?'

fetch(api).then(res => res.json()).then(res => {
    // 请求成功的响应 
    console.log(res)
}).catch(err => {
    // 如果有错误,这里处理
    console.log(err)
})

  
<button>fetch发送请求</button>
<script>
    const api_login = 'http://xxx.com/api/login'
    const data = {username:'admin', password:'123456'}

    const btn = document.querySelector('button')
    btn.addEventListener('click', () => {
        fetch(api_login, {
            method:'post',
            // 1. 要注意设置headers请求头
            headers:{
                'content-type': 'application/json'
            },
            // 2. 参数是写到body这个属性上的
            body: JSON.stringify(data)
        }).then(res => res.json()).then(res => {
            console.log(res)
        }).catch(err => {
            console.log(err)
        })
    })
</script>

判断一个对象是否是isPlainObject

  1. 使用 Object.prototype.toString.call(obj) 方法
javascript
function isPlainObject(obj) {
  return Object.prototype.toString.call(obj) === '[object Object]';
}

**Object.prototype.toString.call(obj) **可以准确获取对象的内部属性[[Class]]的特性。对于普通对象,它的内部属性 [[Class]] 是 'Object'。

  1. 使用 typeof 和 Object.getPrototypeOf 方法
javascript
function isPlainObject(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return false;
  }
  return Object.getPrototypeOf(obj) === Object.prototype;
}

这种方法首先检查 obj 是否是一个非空对象,然后使用 Object.getPrototypeOf 方法获取对象的原型。如果原型是 Object.prototype,则表示它是一个普通对象。

实现loadsh.get

_.get 是 Lodash 库中的一个函数,它用于安全地获取嵌套对象或数组中的值。它非常有用,因为它可以处理多层嵌套的情况,并且在获取属性时不会抛出错误,而是返回一个默认值。

javascript
_.get(object, path, [defaultValue])
  • object(Object): 要获取属性值的对象。
  • path(Array|string): 指定要获取的路径,可以是点路径或数组路径。
  • [defaultValue](*): 如果无法解析指定的路径,则返回这个值。
javascript
// 1. 从嵌套对象中获取值
const user = { name: 'John', address: { city: 'New York' } };
console.log(_.get(user, 'address.city')); // 输出 "New York"
console.log(_.get(user, 'address.country', 'USA')); // 输出 "USA"

// 2. 从嵌套数组中获取值
const data = [{ name: 'Alice' }, { name: 'Bob' }];
console.log(_.get(data, '1.name')); // 输出 "Bob"
console.log(_.get(data, '[1].name')); // 输出 "Bob"

// 3. 使用默认值
const user = { name: 'John' };
console.log(_.get(user, 'address.city', 'Unknown')); // 输出 "Unknown"

// 模拟实现,要求支持 a[0].b格式
get({ a: [{ b: 1 }]}, 'a[0].b', 3)
// output: 1

具体实现

javascript
function get(obj, path, defaultValue = undefined) {
    const prepaths = Array.isArray(path)
        ? path
        : path.replace(/\[(\d+)\]/g, '.$1').split('.')

    // a[3].b -> a.3.b
    // path.replace(/\[(\d+)\]/g, '.$1').split('.')
    // 但是这种,要过滤数组中为空的元素
    // [1].name =>   ['', 1, name ]
    const paths = prepaths.filter((el) => el)
    console.log(paths)

    let result = obj
    for (const p of paths) {
        if (!result) {
            return defaultValue
        }

        result = result[p]
    }

    return result === undefined ? defaultValue : result
}
// 测试
const object = { a: [{ b: { c: 3 } }] }

console.log(get(object, 'a[0].b.c'))
console.log(get(object, ['a', '0', 'b', 'c']))
console.log(get(object, 'a.b.c', 'default'))

const data = [{ name: 'Alice' }, { name: 'Bob' }]
console.log(get(data, '1.name')) // 输出 "Bob"
console.log(get(data, '[1].name')) // 输出 "Bob"

// 因为正则匹配的时候 [1].name  最后数组是 ['', 1, name]

最终版本

javascript
function get(obj, path, defaultValue = undefined) {
    const prepaths = Array.isArray(path)
        ? path
        : path.replace(/\[(\d+)\]/g, '.$1').split('.')


    const paths = prepaths.filter((el) => el)
    // console.log(paths)

    let result = obj
    for (const p of paths) {
        if (!result) {
            return defaultValue
        }

        result = result[p]
    }

    return result === undefined ? defaultValue : result
}

实现LRUCache

最近最少使用算法

利用Map对象的有序性来实现LRU

https://github.com/sisterAn/JavaScript-Algorithms/issues/7前端进阶算法3:从浏览器缓存淘汰策略和Vue的keep-alive学习LRU算法 - 掘金力扣(LeetCode)

Map 既能保存键值对,并且能够记住键的原始插入顺序

javascript
class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.cache = new Map();
  }

  get(key) {
    if (this.cache.has(key)) {
        // 存在即更新
        let temp = this.cache.get(key)
        this.cache.delete(key)
        this.cache.set(key, temp)
        return temp
    }
    return -1
  }

  put(key, value) {
    if (this.cache.has(key)) {
        // 存在即更新(删除后加入)
        this.cache.delete(key)
    } else if (this.cache.size === this.capacity) {
        // 不存在即加入
        // 缓存超过最大值,则移除最近没有使用的
      this.cache.delete(this.cache.keys().next().value);
    }
    this.cache.set(key, value);
  }
}

这个实现主要利用了 Map 对象的有序性质,即最近插入或访问的键值对会被移动到 Map 的末尾。

  1. 在 get(key) 方法中,如果键存在于缓存,则先获取其对应的值,然后删除该键值对,再重新插入到缓存中,这样该键值对就会被移动到 Map 的末尾。
  2. 在 put(key, value) 方法中,如果键已存在,则先删除它,这样它就会被移动到 Map 的末尾。如果键不存在且缓存已满,则需要删除 Map 中的第一个键值对(最久未使用的),然后再插入新的键值对。
  3. Map.keys().next().value 这一行代码用于获取 Map 中的第一个键,即最久未使用的键。Map.keys() 返回一个迭代器,通过 next() 方法可以获取迭代器的下一个值,该值是一个包含 value 和 done 属性的对象,我们取其 value 属性即可获得第一个键。

这种实现的时间复杂度为 O(1),因为 Map 的 get、set、delete 和迭代器操作均为常数时间复杂度。空间复杂度为 O(capacity),因为需要存储 capacity 个键值对。 相比使用额外的 lruList 来记录最近使用的键的顺序,这种实现更加简洁,但缺点是无法直接遍历缓存中的键值对。如果需要遍历功能,则需要使用之前提到的基于双向链表或 lruList 的实现方式。

实现每隔一秒打印 1,2,3,4

javascript
// 注意,for循环先执行完,之后宏任务 setTimeout执行
// let 块级作用域
// 下标注意1开始循环,要改为1
for (let i = 1; i < 5; i++) {
    setTimeout(() => {
        console.log(i)
    }, i * 1000)
}

// 使用闭包实现
for (var i = 1; i < 5; i++) {
  (function(i) {
    setTimeout(function() {
      console.log(i);
    }, i * 1000);
  })(i);
}

使用setTimeout模拟实现setInterval

multiRequest(urls, maxNum)

实现一个批量请求函数 multiRequest(urls, maxNum),要求如下:
• 要求最大并发数 maxNum
• 每当有一个请求返回,就留下一个空位,可以增加新的请求
• 所有请求完成后,结果按照 urls 里面的顺序依次打出

http1.1 , http2

HTTP/1.1 对于同一域名,默认并发连接限制是6个 HTTP/2 支持在单一TCP连接上复用多个请求,所以Chrome对单个域名的连接数限制放宽,默认并发请求限制在1000左右

并发控制的实际应用

例如一个爬虫程序,可以通过限制其并发任务数量来降低请求频率,从而避免由于请求过于频繁被封禁问题的发生。

Promise

异步难题:前端并发控制全解析 - 掘金字节跳动面试官:请用JS实现Ajax并发请求控制 - 掘金JavaScript 中如何实现并发控制? - 掘金阿里面试官的”说一下从url输入到返回请求的过程“问的难度就是不一样! - 掘金 目前来说,Promise是最通用的方案,一般我们最先想到Promise.all,当然最好是使用新出的Promise.allsettled

javascript
function multiRequest(urls = [], maxNum) {
  // 请求总数量
  const len = urls.length;
  // 根据请求数量创建一个数组来保存请求的结果
  const result = new Array(len).fill(false);
  // 当前完成的数量
  let count = 0;
 
  return new Promise((resolve, reject) => {
    // 请求maxNum个
    while (count < maxNum) {
      next();
    }
    function next() {
      let current = count++;
      // 处理边界条件
      if (current >= len) {
        // 请求全部完成就将promise置为成功状态, 然后将result作为promise值返回
        !result.includes(false) && resolve(result);
        return;
      }
      const url = urls[current];
      fetch(url)
        .then((res) => {
          // 保存请求结果
          result[current] = res;
        })
        .catch((err) => {
          result[current] = err;
        }).finally(()=>{
            // 请求没有全部完成, 就递归
          if (current < len) {
            next();
          }
        });
    }
  });
}

异步控制并发数

JS实现一个带并发限制的异步调度器Scheduler,保证同时运行的任务最多有两个。完善下面代码的Scheduler类,使以下程序能够正常输出:

javascript
class Scheduler {
  add(promiseCreator) { ... }
  // ...
}
   
const timeout = time => new Promise(resolve => {
  setTimeout(resolve, time);
})
  
const scheduler = new Scheduler();
  
const addTask = (time,order) => {
  scheduler.add(() => timeout(time).then(()=>console.log(order)))
}

addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');

// output: 2 3 1 4

整个的完整执行流程:

  1. 其实1、2两个任务开始执行
  2. 500ms时,2任务执行完毕,输出2,任务3开始执行
  3. 800ms时,3任务执行完毕,输出3,任务4开始执行
  4. 1000ms时,1任务执行完毕,输出1,此时只剩下4任务在执行
  5. 1200ms时,4任务执行完毕,输出4

实现promise相关

数组相关

数组扁平化

新增的flat

javascript
flat()
flat(depth)
// depth默认为1,如果要拍平为1,提前知道数组维度,
// 1. depth传参 => flat(维度-1)
// 无需知道维度  一哈说
// 2. flat(Infinity)
javascript
const res = arr.flat(Infinity)

for循环递归

javascript
let arr = [1, [2, [3, 4]]]

function flatten(arr) {
    let result = []
    for (let i = 0; i < arr.length; i++) {
        // 对于每个元素arr[i] 判断是否是数组
        if (Array.isArray(arr[i])) {
            // 如果是,递归调用flatten,将结果与result数组进行拼接
            result = result.concat(flatten(arr[i]))
        } else {
            // 如果不是,直接push到result中
            result.push(arr[i])
        }
    }
    return result
}

console.log(flatten(arr))

// 改为forEach
function flatten(arr) {
    let result = []
    arr.forEach((el) => {
        if (Array.isArray(el)) {
            result = result.concat(flatten(el))
        } else {
            result.push(el)
        }
    })
    return result
}

reduce + concat 递归实现

javascript
const arr = [1, [2, 3], [4, [5, 6]]]
const flatten = (arr) => {
    return arr.reduce((acc, cur) => {
        return acc.concat(Array.isArray(cur) ? flatten(cur) : cur)
    }, [])
}
console.log(flatten(arr))

栈结构 + while

javascript
function flatten(arr) {
    // 将原数组展开 推入栈中
    const stack = [...arr]
    const res = []
    // 不断从栈中取出元素
    while (stack.length) {
        // 取出栈顶元素next
        const next = stack.pop()
        if (Array.isArray(next)) {
            // 如果是数组,将数组展开一层后再推入栈
            stack.push(...next)
        } else {
            // 当执行入栈结束后 stack = [1, 2, 3, 4]
            // 这时每次从栈顶取一个放到res中 [4] [4,3] ...
            res.push(next)
        }
    }

    // [4,3,2,1]
    return res.reverse()
}
console.log(flatten(arr))

PS: 用数组模拟栈结构

在用数组模拟栈结构时,栈顶元素就是数组的最后一个元素。 栈(stack)是一种后进先出(LIFO, Last In First Out)的线性数据结构。它有两个基本操作:

  1. 入栈(push):将一个元素压入栈顶
  2. 出栈(pop):从栈顶移除一个元素

在使用数组来模拟栈结构时,我们约定:

  • 入栈时,使用 push 方法将元素添加到数组末尾,作为栈顶元素。
  • 出栈时,使用 pop 方法移除并返回数组的最后一个元素,即栈顶元素。

例如,我们有一个栈 stack = [](空数组),执行以下操作:

javascript
javascriptCopy code

stack.push(1); // 入栈1, stack = [1]
stack.push(2); // 入栈2, stack = [1, 2]
stack.push(3); // 入栈3, stack = [1, 2, 3]

const top = stack.pop(); // 出栈, top = 3, stack = [1, 2]

可以看到,每次入栈时,我们都是将元素添加到数组末尾;出栈时,我们移除并返回数组的最后一个元素。这就确保了栈顶元素总是数组的最后一个元素。

数组去重

使用set + 扩展运算符 / Array.from

javascript
const uniq = [...new Set(arr)]
// 由于 Set 的集合元素唯一的特性,它会自动去除重复的值。
// 然后使用扩展运算符将 Set 转换回数组。

const uniq = Array.from(new Set(arr))

使用 filter + indexOf

返回 item 第一次出现的位置等于当前的index的元素

javascript
const arr = [1, 2, 2, 3]
const uniq = arr.filter((item, index) => arr.indexOf(item) === index )
1 0 
2 1 
2 2  // item 2  =>  index找到了1 过滤到这个
3 3  // item 3  =>  index 3 (第一次出现时,indexOf(item)结果 和 index必然相等)

reduce + includes

reduce复习 对数组里的每个元素都执行一个自定义的reducer函数,将其结果汇总为单个返回值。

javascript
// 语法: arr.reduce(callbackFn, initialValue)
// 作用: 对数组里的每个元素都执行一个自定义的reducer函数,将其结果汇总为单个返回值。
// 参数: 
// callbackFn               :  回调函数   必须     √
// initialValue             :  初始值     (可选)  √

// callbackFn的参数:    
//       previousValue      :  上一次调用callbackFn的返回值    √
//       currentValue       :  当前元素                      √
//       currentIndex       :  当前元素的索引  (可选) 不要求
//       array              :  源数组  (可选)      不要求

// 返回值: 使用reducer回调函数遍历整个数组后的结果

// 注意: 如果有初始值,那么prev第一次执行的时候,就是写的初始值
//       如果没有初始值,initValue就是数组的第一个元素 arr[0], cur就依次取第二个元素

const arr = [1, 2, 3]
const res = arr.reduce((pre, cur) => {
    return pre + cur 
}, 8)
// console.log(res)

const res3 = arr.reduce((pre,cur) => pre + cur, 8)
console.log(res3)


const res1 = arr.reduce(function(pre, cur){
    // console.log(pre)
    // 1 + 2   ==> 3 
    // 3 + 3   ==> 6
    return pre + cur 
})
console.log(res1)
javascript
const uniqueArray = arr.reduce(
    //    => 后半段分析,  前面的累加器包含当前元素吗?包含,返回acc数组不做任何改变
    //    acc.includes(cur) => false ; 返回一个新的数组[...acc, cur],
    //  将cur添加到累加器中
    (acc, cur) => (acc.includes(cur) ? acc : [...acc, cur]),
    []
)

// => const uniq = arr.reduce(fn, [])  先写初始值[], 不然容易搞忘
const uniqArray = arr.reduce(
    (acc, cur) => (acc.includes(cur) ? acc : [...acc, cur]),
    []
)

使用Map

利用map键是唯一不重复的 ,如果添加了一个已经存在的键,那么这个键对应的值会被新值覆盖掉。 PS。对象属性的键也是唯一的,和map一样,已存在,则更新。

javascript
const arr = [1, 2, 2, 333, 333, 555, 666]
const uniqArray = [...new Map(arr.map((el) => [el, el])).keys()]
console.log(uniqArray)

利用reduce+对象属性

对象属性键唯一性,key不可重复

javascript
const uniqueArray = Object.keys(
    arr.reduce((acc, cur) => {
        acc[cur] = true
        return acc
    }, {})
)

数组对象去重

利用数组中对象的某个 key 作为唯一标识符来去重,

使用Map

javascript
const uniqueArray = [...new Map(arr.map(obj => [obj.key, obj])).values()];

首先使用 map 方法将原数组中的对象映射为 [key, obj] 形式的数组,然后使用 new Map 创建一个 Map 对象,由于 Map 的键是唯一的,因此会自动去重。最后使用扩展运算符将 Map 对象转换回数组。

使用reduce

javascript
const uniqueArray = arr.reduce((acc, obj) => {
  const key = obj.key;
  if (!acc.some(item => item.key === key)) {
    acc.push(obj);
  }
  return acc;
}, []);
// acc.some(fn) 看是否至少有一个元素通过了测试,返回boolean

使用 reduce 遍历原数组,对于每个对象,先获取它的 key 值,然后使用 some 方法检查结果数组中是否已经存在具有相同 key 值的对象。如果不存在,就将该对象添加到结果数组中。

数组交集

所有属于集合A且属于集合B的元素所组成的集合,叫做集合A与集合B的_交集_

filter+includes

javascript
const inte = (arr1, arr2) => arr1.filter(item => arr2.includes(item));

filter + set (和includes原理类似)

javascript
const inte = (arr1, arr2) => arr1.filter(item => new Set(arr2).has(item));

filter + indexOf

javascript
const inte = (arr1, arr2) => arr1.filter(item => arr2.indexOf(item) !== -1);

数组差集

差集指的是从一个集合中去掉另一个集合的元素所得到的剩余部分。

对于两个集合 A 和 B,A 与 B 的差集表示为 A - B,它包含了所有属于集合 A 但不属于集合 B 的元素。 用数学符号表示, A - B = {x | x ∈ A 且 x ∉ B}。 以两个数组为例:

javascript
A = [1, 2, 3, 4, 5]
B = [4, 5, 6, 7]

那么 A 与 B 的差集 A - B 就是:

javascript
A - B = [1, 2, 3]

即从数组 A 中去掉所有也出现在数组 B 中的元素 4 和 5,剩下的就是差集 [1, 2, 3]。 差集常常用于许多实际应用场景中,比如:

  • 获取两个用户列表中,只存在于 A 列表而不在 B 列表中的用户。
  • 对比两个文件的内容差异,找出只存在于文件 A 中而不在文件 B 中的行。
  • 对比两个数据库表的数据,找出在表 A 中但不在表 B 中的记录。

filter + includes

javascript
// 属于arr1 , 且不属于 arr2 
const diff = (arr1, arr2) => arr1.filter(item => !arr2.includes(item));

filter + indexOf

javascript
const diff = (arr1, arr2) => arr1.filter(item => arr2.indexOf(item) === -1 )

数组并集

先将两个数组展开到一个新数组中,然后使用 Set 去重,最后使用扩展运算符或者Array.from将 Set 转换回数组。

javascript
// 1. set 和 展开运算符
const union = (arr1, arr2) => [...new Set([...arr1, ...arr2])];

// 2. set 和 Array.from
const union = (arr1, arr2) => Array.from(new Set([...arr1, ...arr2]));
const union = (arr1, arr2) => Array.from(new Set(arr1.concat(arr2)));

// 3. filter 和 indexOf / includes 
// arr1 合并上  arr2中没有arr1元素,arr2过滤后的
const union = (arr1, arr2) => arr1.concat(arr2.filter(item => arr1.indexOf(item) === -1));
const union = (arr1, arr2) => arr1.concat(arr2.filter(item => !arr1.includes(item)));

伪数组转数组

有索引,有length,没有数组的push等内置方法

Array.from()

javascript
const arrayLike = {0: 'a', 1: 'b', 2: 'c', length: 3};
const array = Array.from(arrayLike);
console.log(array); // ['a', 'b', 'c']

展开运算符...

javascript
const arrayLike = {0: 'a', 1: 'b', 2: 'c', length: 3};
const array = [...arrayLike];
console.log(array); // ['a', 'b', 'c']

Array.prototype.slice.call()

原理: slice方法从Array.prototype来,当使用**Array.prototype.slice.call(arguments)时,**因为arguments是一个伪数组对象,没有继承Array.prototype上的slice方法。 所以,通过call,将slice方法的执行上下文(this) 设置为arguments这个伪数组对象。 然后,slice内部就会将arguments转为一个真正的数组,因为slice需要获取索引和length;最终slice方法返回一个新的真正的数组。

javascript
const arrayLike = {0: 'a', 1: 'b', 2: 'c', length: 3};
const array = Array.prototype.slice.call(arrayLike)
console.log(array); // ['a', 'b', 'c']

手写实现new

JavaScript 中的 new 运算符用于创建一个新的对象实例。当使用 new 与构造函数一起调用时,会经历以下几个步骤:

  1. 创建一个新对象 obj。
  2. 将新对象的原型指向构造函数的原型。
  3. 将 this 指向新对象 obj。
  4. 执行构造函数代码,为这个新对象添加属性。
  5. 如果构造函数没有手动返回对象,则默认返回 this,也就是新创建的对象 obj。
javascript
function _new(fn,...args){
    // let obj = new Object()
    // obj.__proto__ = fn.prototype
    // 基于fn构造函数原型创建一个新对象
    let obj = Object.create(fn.prototype)
    // 执行构造函数,并获取fn执行的结果
    let res = fn.call(obj,...args) 
    // 如果执行结果有返回值并且是一个对象,返回执行结果,否则,返回新创建的对象
    if (res && (typeof res === 'object' || typeof res === 'function')) retrun res
    return obj
}

// 更好理解
function myNew(fn, ...args) {
  // 1. 创建新对象,并让新对象的原型指向构造函数的原型
  let obj = Object.create(fn.prototype)
  // 2. 让this指向新对象(call),执行构造函数,为这个新对象添加属性
  let res = fn.call(obj,...args)
  // 3. 如果执行结果有返回值并且是一个对象,返回执行结果,否则,返回新创建的对象obj
  if (res && (typeof res === 'object' || typeof res === 'function')) retrun res
  return obj
}

测试

javascript
function Person(name, age) {
  this.name = name;
  this.age = age;
}

const person1 = myNew(Person, '张三', 25);
console.log(person1.name); // 张三
console.log(person1.age); // 25

const person2 = new Person('李四', 30);
console.log(person2.name); // 李四
console.log(person2.age); // 30

instanceof

  • Instanceof: 用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
  • 通俗一点就是: 判断new出的实例对象是否是当前构造函数生成的对象
javascript
function my_instanceof(left, right) {
  // 这里先用typeof来判断基础数据类型,如果是,直接返回false
  if(typeof left !== 'object' || left === null) return false;
  // getProtypeOf是Object对象自带的API
  // 返回指定对象的原型(内部[[Prototype]]属性的值)隐式原型
  let proto = Object.getPrototypeOf(left);
  let prototype = right.prototype
  while(true) {                  //循环往下寻找,直到找到相同的原型对象
    if(proto === null) return false;
    if(proto === prototype) return true;//找到相同原型对象,返回true
    proto = Object.getPrototypeof(proto);
    }
}

// 更好理解版本
function myInstanceof(left, right) {
    if(typeof left !== 'object' || left === null) return false;

  	let proto = left.__proto__
    let prototype = right.prototype // 右边的原型
    while (true) {
        if (proto === null) return false
        if (proto === prototype) return true
        proto = proto.__proto__ // 向上查找,直到proto为null
    }
}

记下下面这个!

javascript
// instanceof 运算符    instance 实例 of 谁的
// 语法 : A instanceof B
//        实例 ----->  构造函数
// 作用: 用于检测构造函数的原型是否出现在某个实例对象的原型链上。
// 理解:
//  ===>用于检测A这个实例(对象)是不是B这个构造函数创建的,或者是不是B这个祖先构造函数创建的
javascript
// 2024.3.27
const isObject = (obj) => {
    return typeof obj === 'object' && obj !== null
}
function myInstanceOf(obj, constructor) {
    // 1. 如果左边待判断的obj不是对象,直接返回false
    if (!isObject(obj)) return false

    // 2. 获取对象的原型
    // let proto = obj.__proto__
    let proto = Object.getPrototypeOf(obj)
    // 3. 获取构造函数的prototype对象
    let prototype = constructor.prototype

    // 4. 循环查找对象的原型
    while (true) {
        if (proto === null) return false
        // 如果原型等于构造函数的 prototype 对象,则说明是该构造函数的实例
        if (proto === prototype) return true
        // 否则,继续查找原型的原型
        // proto = proto.__proto__
        proto = Object.getPrototypeOf(proto)
    }
}

// 定义构造函数
function Animal(name) {
    this.name = name
}

// 创建实例
const dog = new Animal('狗狗')
const cat = new Animal('猫猫')
const obj = {}

// 使用手写的 myInstanceOf 函数
console.log(myInstanceOf(dog, Animal)) // true
console.log(myInstanceOf(cat, Animal)) // true
console.log(myInstanceOf(obj, Animal)) // false
console.log(myInstanceOf(dog, Object)) // true
console.log(myInstanceOf(obj, Object)) // true
console.log(obj instanceof Animal) // false

模拟实现Objecte.create()

Object.create() 静态方法以一个现有对象作为原型,创建一个新对象。

javascript
// 语法
// Object.create(proto)
// proto : 新创建对象的原型对象。
const person = {
  isHuman: false,
  printIntroduction: function () {
    console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
  },
};

const me = Object.create(person);

me.name = 'Matthew'; // "name" is a property set on "me", but not on "person"
me.isHuman = true; // Inherited properties can be overwritten

me.printIntroduction();
// Expected output: "My name is Matthew. Am I human? true"

可以使用 Object.create() 来模仿 new 运算符的行为。

javascript
function Constructor() {}
o = new Constructor();
// 等价于:
o = Object.create(Constructor.prototype);

模拟实现Object.create()

javascript
function myObjectCreate(proto) {
  // 创建一个临时构造函数
  function Func() {}
  // 将传入的原型对象赋值给临时构造函数的原型
  Func.prototype = proto;
  // 使用 new 运算符创建一个 Func 的实例对象,并返回这个对象
  // 该对象的原型指向传入的proto对象
  return new Func();
}

// 创建一个对象,以proto为新创建对象的原型对象

举例

javascript
// 创建一个原型对象
const personPrototype = {
  greet() {
    console.log(`Hello, my name is ${this.name}`);
  }
};



// 创建一个继承自 personPrototype 的新对象
const john = myObjectCreate(personPrototype);
john.name = 'John'; // 添加属性
john.greet(); // 输出: Hello, my name is John

//-------------------
// 带入源码中,相当于
// john = new Func()
// 那么实例john的隐式原型 就是 Func.prototype =>也就是proto
// 换句话说,实例的原型对象就是proto
// john.__proto__ = proto
//-------------------

// 检查原型链
console.log(Object.getPrototypeOf(john) === personPrototype); // true

手写函数柯里化

柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术 在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。


柯里化(Currying)是一种函数的转换技术,它可以将一个接收多个参数的函数转换为一个接收单一参数的函数序列。每次调用新函数时,只需要传入一个参数,并返回一个新函数来接收剩余的参数,直到所有参数传入完毕后,最终执行原函数。


柯里化的过程可以分为多个步骤:

  1. 接收一部分参数,返回一个新的函数。
  2. 新函数持有已接收的参数,并等待剩余参数的传入。
  3. 当所有参数都传入后,执行原函数,并返回最终结果。

柯里化的作用和优点: ()

  1. 参数复用:柯里化可以缓存参数,避免多次重复传入相同的参数。
  2. 延迟执行:可以控制函数的执行时机,在所有参数传入后才执行。
  3. 函数组合:通过柯里化生成的新函数,可以更容易地组合和复用。
  4. 代码简洁:柯里化可以让代码变得更加简洁、易读和可维护。

其他解释 作用:参数复用提前返回延迟执行 提前返回 和 延迟执行 也很好理解,因为每次调用函数时,它只接受一部分参数,并返回一个函数(提前返回),直到(延迟执行)传递所有参数为止。

javascript
// 普通的非柯里化函数
function sum(a, b, c) {
  return a + b + c;
}

console.log(sum(1, 2, 3)); // 输出 6

如何实现一个add函数,使得这个add函数可以灵活调用和传参,支持下面的调用示例呢?

javascript
add(1, 2, 3) // 6
add(1) // 1
add(1)(2) // 3
add(1, 2)(3) // 6
add(1)(2)(3) // 6
add(1)(2)(3)(4) // 10

彻底搞懂闭包,柯里化,手写代码,金九银十不再丢分! - 掘金

柯里化的本质是闭包(保存参数)

javascript
// 直接传fn的方式
function curry(fn) {
    return function curried(...args) {
        // 如果传入的参数足够执行函数,则直接执行
        if (args.length >= fn.length) {
            return fn(...args)
        }

        // 否则,返回一个新的柯里化函数 递归
        return (...newArgs) => curried(...args, ...newArgs)
    }
}

// 或者 简洁版 ==> 记忆这一版本
function currying(fn) {
  const curry = function(...args) {
    return args.length >= fn.length
      ? fn(...args)
      : (...newArgs) => curry(...args, ...newArgs);
  };
  return curry;
};
// 2. 传参方式不同 
const curry = (fn, ...args) =>
    // 当参数与fn参数相同,则直接执行函数 fn.length 形参个数
    args.length >= fn.length
        ? fn(...args)
        : // 如果不够,返回一个新的柯里化函数
          (...newArgs) => curry(fn, ...args, ...newArgs)


const curriedSum = curry(sum)

console.log(curriedSum(1, 2, 3)) // 输出 6
console.log(curriedSum(1)(2)(3)) // 输出 6
console.log(curriedSum(1, 2)(3)) // 输出 6
console.log(curriedSum(1)(2, 3)) // 输出 6
typescript
function currying(fn) {
  // 返回一个函数
  const curry = function(...args) {
    // 比较参数,如果传入参数>= fn.length 执行,否则继续递归
    return args.length >= fn.length
      ? fn(...args)
      : (...newArgs) => curry(...args, ...newArgs);
  };
  return curry;
};

手写sum相关

实现一个add

typescript
add(1, 2, 3) // 6
add(1) // 1
add(1)(2) // 3
add(1, 2)(3) // 6
add(1)(2)(3) // 6
add(1)(2)(3)(4) // 10
typescript
function add(...args) {
  const func = add.bind(null, ...args)
  func.valueOf = () => {
    return args.reduce((acc, cur) => acc + cur, 0)
  }
  return func
}

console.log(+add(1)(2))
console.log(+add(1, 2)(3))
console.log(+add(1)(2)(3))

实现一个 sum 函数如下所示:

typescript
sum(1, 2, 3).valueOf(); //6
sum(2, 3)(2).valueOf(); //7
sum(1)(2)(3)(4).valueOf(); //10
sum(2)(4, 1)(2).valueOf(); //9
sum(1)(2)(3)(4)(5)(6).valueOf(); // 21
typescript
// 方式1 
function sum(...args) {
  const func = sum.bind(null, ...args);  
  // 定义 valueOf 方法,其作用是计算之前所有传入的参数的和
  func.valueOf = function() {
    return args.reduce((a, b) => a + b);
  };
  return func;  // 返回新的函数,以支持链式调用
}

// 方式2
function sum(...args) {
  const f = (...rest) => sum(...args, ...rest);
  f.valueOf = () => args.reduce((x, y) => x + y, 0);
  return f;
}

如果不使用 valueOf(sumof一样)_,_可直接进行计算,如下示例,应如何处理。

typescript
//=> 15
sum(1, 2, 3) + sum(4, 5);
 
//=> 100
sum(10) * sum(10);

Symbol.toPrimitive

通过自定义函数对象的 Symbol.toPrimitive 方法。Symbol.toPrimitive 是一个内置的 Symbol 值,它的作用是转换一个对象为相应的原始值。 当尝试将函数对象 func 转换为原始值(比如在进行数学运算或字符串连接的时候)时,JavaScript 就会自动调用这个方法,并使用它的返回值代替对象本身。(上面执行加法或者乘法就会调用)


假如你有一个对象,并且当这个对象在执行数值或字符串操作时,你想要让它表现得像一个基本数据类型,那么你可以提供一个 Symbol.toPrimitive 方法。这个方法接受一个字符串参数,表示所期望的转换类型,可以是 "number"、"string" 或者 "default"。 在上述 sum 函数的实现中,我们使用 Symbol.toPrimitive 方法将函数对象转换为一个数字,也就是所有参数的和。这就意味着当在数字操作中使用这个函数对象时,JavaScript 会自动调用这个方法,然后使用其返回的数值。

typescript
function sum(...args) {
  //  写法一 利用bind保存函数
  //   const func = sum.bind(null, ...args)
  //   写法二
  //   创建新函数func,并调用sum函数来进行参数的传递和保存
  const func = (...rest) => sum(...args, ...rest)
  func[Symbol.toPrimitive] = function () {
    return args.reduce((a, b) => a + b, 0)
  }
  return func
}

console.log(sum(1, 2, 3) + sum(4, 5)) // 输出 15
console.log(sum(10) * sum(10)) // 输出 100

手写 call / apply / bind

  • 共同点:都是改变this指向
  • 区别
    • call 和 apply是立即执行的,而bind是返回一个新的函数,需要手动去调用
    • call可以传递多个参数,第一个参数和apply'一样,是用来替换的对象,后边是参数列表
    • apply最多只能有两个参数 (新this对象,数组argsArray)-> fun.apply(thisArg, [argsArray])

call的实现

javascript
Function.prototype.myCall = function (ctx, ...args) {
    // 当传入的第一个参数ctx(this要指向的对象)为null/undefined,让this指向window
    // 如果值存在,但是是数字字符串什么的,转为实例
    // 简单处理
    ctx = ctx ? Object(ctx) : window
    // 用symbol生成唯一的key
    const fn = Symbol()
    // 首先要获取调用call的函数,用this可以获取
    ctx[fn] = this // ctx[fn] 就是要调用的函数名
    // 2.将挂载后的方法调用
    const res = ctx[fn](...args)
    // 3.用完以后,将fn从ctx中删除
    delete ctx[fn]
    // 4. 返回调用结果
    return res
}
javascript
// 判断ctx上下文时,以下更完善一点,但不用这么完美,我们简单判断就行
ctx = ctx ? Object(ctx) : window

// 完善一点版本
if (context === null || context === undefined) {
    // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
    context = window
} else {
    // 暴力处理 ctx有可能传非对象
    context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
}

call原理分析

javascript
// 1. call的使用
var foo = {
    value: 1
}
function bar() {
    console.log(this.value)
}
bar.call(foo) // 1  
// 这里call 让bar函数执行的时候,内部this指向了foo

// 我们怎么模拟这个效果呢?改造一下call的那个对象obj的代码


// 2. 改造成 =》
var foo = {
    value: 1,
    bar: function () {
        console.log(this.value)
    }
}
// call谁,给谁添加call前面的方法
// bar.call(foo), 给foo这个对象添加bar方法
foo.bar() // bar函数中,this指向的就是foo,
// 就和我们要的改变this指向功能符合了,并且bar函数也调用执行了
javascript
// 3. 同理
function greet(greeting, message) {
  console.log(`${greeting}, ${this.name}. ${message}`);
}

const obj = { name: '张三' };

greet.myCall(obj, '你好', '欢迎使用 myCall');

// 4. greet.mycall(obj)我们可以改成
//  给obj添加一个greet方法
const obj = {
    name: '张三',
    greet: function (greeting, message) {
        console.log(`${greeting}, ${this.name}. ${message}`)
    }
}
obj.greet()
// 当调用这个greet方法时,greet内部的this指向的就是obj了

// 5. 回到
// greet.myCall(obj, '你好', '欢迎使用 myCall');
Function.prototype.myCall = function(context) {
    // 首先要获取调用call的函数,用this可以获取, 
    // 即在greet.myCall(obj),再myCall内部,this指向greet
    // (谁调用,this指向谁)
    context.fn = this;

    // 而在执行 context.fn() 内部,fn()内部的this又指向了context
    // 这里就实现了我们要改变this指向的目的!
    context.fn();
    delete context.fn;
}

// 要调用myCall的函数是greet,怎么获取呢 this就是greet

在这个例子中: 在 myCall 函数内部:

  1. this 指向 greet 函数本身,即 this 是 greet 函数。
  2. context 是传入的 obj 对象。
  3. 在函数内部,我们给 context (也就是 obj) 添加了一个临时属性 fn,并将 this (即 greet 函数)赋值给它,也就是 obj.fn = greet。
  4. 接着执行 context.fn(...args),也就是 obj.fn('你好', '欢迎使用 myCall')。这里的 fn 指向的就是 greet 函数,所以相当于执行 greet('你好', '欢迎使用 myCall')。
  5. 但由于执行时的 this 被指向了 obj对象,所以 greet 函数中的 this.name 取到的就是 obj.name 的值,即 '张三'。

apply手写

该方法的语法和作用与 call 方法类似,只有一个区别,就是 call 方法接受的是一个参数列表,而 apply 方法接受的是一个包含多个参数的数组。

javascript
// 唯一的区别在这里,不需要apply接收两个参数,第二个参数是数组
/**
 *
 * @param {*} ctx 函数执行上下文this
 * @param {*} args  参数列表
 * @returns 函数执行的结果
 */
// 唯一的区别在这里,args变成数组
Function.prototype.myApply = function (ctx, args) {
    ctx = ctx ? Object(ctx) : window

    // 创造一个唯一的方法名,并让这个方法指向调用时fn.apply(),apply前面的fn
    // 而this刚好这里就是指向fn
    const fn = Symbol()
    ctx[fn] = this
    // 将args参数数组,展开为多个参数,供函数调用
    const res = ctx[fn](...args)
    delete ctx[fn]
    return res
}

手写bind实现

bind 函数用于创建一个新的函数,该函数被调用时其 this 值会被绑定到传递给 bind 函数的值。

javascript
Function.prototype.myBind = function(context, ...args) {
  // 保存this,即被调用的函数
  const self = this;

  // 返回一个新的函数
  return function(...newArgs) {
    // 将 args 和 newArgs 合并为最终的参数列表
    const finalArgs = [...args, ...newArgs];

    // 调用原函数,并绑定 this 为 context
    return self.apply(context, finalArgs);
  }
}

关键点:

  1. 使用self保存原函数的 this。
  2. 返回一个新的函数,该函数在执行时将 this 绑定到指定的上下文对象,并合并参数列表。
  3. 使用 apply 或 call 调用原函数,将绑定的 this 和合并后的参数传递给原函数。
javascript
// 使用示例
const obj = { name: '张三' };

function greet(greeting, message) {
  console.log(`${greeting}, ${this.name}. ${message}`);
}

const boundGreet = greet.myBind(obj, '你好');
boundGreet('欢迎使用 myBind'); // 输出: 你好, 张三. 欢迎使用 myBind

const anotherBoundGreet = greet.myBind(obj, '早上好');
anotherBoundGreet('今天天气不错'); // 输出: 早上好, 张三. 今天天气不错

bind-best!

javascript
// 2024.3.27
Function.prototype.myBind = function(context, ...args) {
  // 判断调用对象是否为函数 因为 bind 方法只能被函数对象调用。
  if (typeof this !== 'function') {
    throw new TypeError('Function.prototype.myBind was called on non-function');
  }

  // 保存this,即被调用的函数
  const self = this;

  // 创建一个新的函数对象
  const fBound = function(...newArgs) {
    // 合并参数列表
    const finalArgs = [...args, ...newArgs];

    // 如果通过 new 运算符调用,this 会指向新创建的实例对象
    // 否则,this 会指向传入的 context
    return self.apply(this instanceof fBound ? this : context, finalArgs);
  }

  // 设置新函数的原型对象,使其能够正确继承原函数的原型
  // 保证新函数在使用 new 运算符时,实例对象能够正确继承原函数的原型。
  fBound.prototype = Object.create(self.prototype);

  return fBound;
}

手写寄生式组合继承!

typescript
function Parent(name, age) {
  this.name = name;
  this.age = age;
}
// 用于验证Child是否可以继承getName方法
Parent.prototype.getName = function () {
  return this.name
}

function Child(name, age, skills) {
  // 这里是组合继承,用call的方式,借用父类构造函数继承了父类的实例属性
  // 优点:子类创建实例可以向父类构造函数传参。
  Parent.call(this, name, age);
  this.skills = skills;
}

// 这里叫寄生继承
// Object.create(Person.prototype) 创建了父类原型的副本
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

---
// Object.create原理
function myCreate(obj) {
  function F() {}
  F.prototype = obj;
  return new F();
}

手写总结

  1. 金三银四:20道前端手写面试题 - 掘金
  2. 字节跳动面试记录 - 掘金
  3. 小厂一面:30分钟速通,拿下一血! - 掘金
  4. 滴滴前端一面高频手写面试题汇总 - 掘金
  5. 前端手写代码汇总 - 掘金
  6. 京东前端二面高频手写面试题(持续更新中) - 掘金
  7. 字节前端高频手写面试题(持续更新中) - 掘金
  8. 前端JS手写编程题库,终于开源了 Github - 掘金
  9. 2023前端二面手写面试题总结 - 掘金
  10. 前端面试常见的手写功能 - 掘金
  11. 2021年前端各大公司都考了那些手写题(附带代码) - 掘金
  12. 【中高级前端】必备,30+高频手写实现及详细答案(万字长文),你值得拥有噢 - 掘金
  13. 前端面试常考的手写代码不是背出来的! - 掘金
  14. 「中高级前端面试」JavaScript手写代码无敌秘籍 - 掘金
  15. 10个常见的前端手写功能,你全都会吗? - 掘金
  16. 「2021」高频前端面试题汇总之手写代码篇 - 掘金

  1. 【中高级前端】必备,30+高频手写实现及详细答案(万字长文),你值得拥有噢 - 掘金
  2. 最全的手写JS面试题 - 掘金
  3. 前22年的Loser,后4年和自己赛跑的人 | 最惨前端面经 - 掘金 kan
  4. 面试官问到三次握手,我甩出这张脑图,他服了! - 掘金 网络
  5. https://mp.weixin.qq.com/s/CFoTRNDXHbqenmW7jFVczg?poc_token=HHa4AmajW2vlFhojYdRcYt-eUxjeetHzEyBrvTmc kan
  6. 前端JS手写编程题库,终于开源了 Github - 掘金