文章目录
- 1、单向链表反转(深信服一面)
- 2、js十大排序算法
- 3、数组去重
- 4、二叉树相关
- 重建二叉树
- 5、数组拍平
- 6、走台阶问题(递归)
- 7、判断一个字符串是否是回文?
- 8、统计一个字符串出现最多的字母
- 9、不借助临时变量,进行两个整数的交换
- 10、斐波那契数列
- 矩形覆盖问题
- 11、输入两棵二叉树A,B,判断B是不是A的子结构
- 12、输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序
- 13、字符串的全排列
- 14、字符串的比较
- 15、给定一个字符串,找出不含有重复字符的最长子串的长度
- 16、动态规划:连续子数组最大和
- 17、和为s的连续正数序列
- 18、和为s的两个数字(夹逼法)
- Promise实现
- 防抖
- 节流
- 深拷贝
- call、apply、bind实现
聊聊面试必考-递归思想与实战
1、单向链表反转(深信服一面)
法一:就地反转法
思路:把当前链表的下一个节点pCur插入到头结点head
的下一个节点中,就地反转。
head->1->2->3->4->5的就地反转过程:
- head->2->1->3->4->5
- head->3->2->1->4->5
- head->4>-3->2->1->5
- head->5->4->3->2->1
function ReverseList(pHead)
{
var p = pHead;
var cur = null; // 指向要操作的当前节点
if(p == null) //做个判断啊!!非常傻的一个错呜呜呜
return null;
while(p.next !== null) {
cur = p.next;
p.next = cur.next;
cur.next = pHead;
pHead = cur;
}
return pHead;
}
新建链表,头节点插入法
思路:新建一个头结点,遍历原链表,把每个节点插入依次插入到新建链表的头结点后。最后,新建的链表就是反转后的链表。
判断链表是否有环(快慢指针):
《有关单链表中环的问题》
var hasCycle = function(head) {
if(!head || !head.next) {
return false
}
let fast = head, slow = head
while (slow != NULL && fast -> next != NULL) {
slow = slow -> next ;
fast = fast -> next -> next ;
if (slow == fast)
return true ;
}
return false ;
};
2、js十大排序算法
冒泡排序
思想:
- 比较相邻的两个元素,如果前一个比后一个大,则交换位置。
- 比较完第一轮的时候,最后一个元素是最大的元素。
- 这时候最后一个元素是最大的,所以最后一个元素就不需要参与比较大小。
function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len-1; i++) {
for (var j = 0; j < len - 1 - i; j++) {
// 相邻元素两两对比,元素交换,大的元素交换到后面
if (arr[j] > arr[j + 1]) {
var temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
return arr;
}
//举个数组
myArr = [20,18,27,19,35];
//使用函数
bSort(myArr)
选择排序
思想:首先从未排序序列中找到最小的元素,放到已排序序列的末尾,重复上述步骤,直到所有元素排序完毕。
//选择排序
function selsetSort(arr){
var len = arr.length;
var index;
for(var i=0;i<len-1;i++){
index=i;
for(var j=i+1;j<len;j++){
if(arr[index]>arr[j]){//寻找最小值
index=j;//保存最小值的索引
}
}
if(index!=i){
var temp =arr[i];
arr[i]=arr[index];
arr[index]=temp;
}
}
return arr;
}
插入排序
思想:
- 从第一个元素开始,该元素可以被认为已经被排序
- 取出下一个元素,在已经排好序的序列中从后往前扫描
- 直到找到小于或者等于该元素的位置
- 将该位置后面的所有已排序的元素从后往前依次移一位(赋值操作)
- 将该元素插入到该位置
- 重复步骤2~5
时间复杂度:
- 最好为o(n)
- 最坏为(n^2) 平均为o(n^2)
- 空间复杂度为o(1) 稳定
function insertSort(arr) {
var len =arr.length;
for (var i=1;i<len; i++) {
var temp=arr[i];
var j=i-1;//默认已排序的元素
while (j>=0 && arr[j]>temp) { //在已排序好的队列中从后向前扫描
arr[j+1]=arr[j]; //已排序的元素大于新元素,将该元素移到一下个位置
j--;
}
arr[j+1]=temp;
}
return arr
}
希尔排序
思想:希尔排序是按一定的间隔对数列进行分组,然后在每一个分组中做插入排序;随后逐次缩小间隔,在每一个分组中做插入排序…直到间隔等于1,做一次插入排序后结束。
时间复杂度一下由O(n^2) 降为 O(nlogn)。
function shellSort(arr) {
var len =arr.length;
gap = Math.floor(len/2);
while(gap!==0){
// 内层循环与插入排序的写法基本一致,只是每次移动的步长变为 gap
for(var i = gap;i<len;i++){
var temp = arr[i];
var j = i-gap;
while(j>=0 && temp<arr[j]){
arr[j+gap] = arr[j];
j-=gap
}
arr[j+gap] = temp;
}
gap=Math.floor(gap/2);
}
return arr;
}
归并排序
思想:采用的是分治的思想,首先是“分”,将一个数组反复二分为两个小数组,直到每个数组只有一个元素;其次是“治”,从最小数组开始,两两按大小顺序合并,直到并为原始数组大小
function mergeSort(arr) { //采用自上而下的递归方法
var len = arr.length;
if(len < 2) {
return arr;
}
var middle = Math.floor(len / 2),
left = arr.slice(0, middle),
right = arr.slice(middle);
return merge(mergeSort(left), mergeSort(right));
}
function merge(left, right)
{
var result = [];
while (left.length>0 && right.length>0) {
if (left[0] <= right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}
while (left.length)
result.push(left.shift());
while (right.length)
result.push(right.shift());
return result;
}
快速排序
思想:挖坑填数 + 分治法
- 选中基准点,开始索引 i ,结束索引 j
- 从数列右边开始往左边找,找到比基准点小的,交换位置 i++
- 从数列左边开始往右边找,找到比基准点大的,交换位置 j–
- 循环执行2、3,直到,i 不是小于 j
时间复杂度:
- 最坏运行情况是O(n²)(顺序数列的快排)
- 平摊期望时间是O(n log n)
// 快排
function quickSort(arr, i, j) {
if(i < j) {
let left = i;
let right = j;
let pivot = arr[left];
while(i < j) {
while(arr[j] >= pivot && i < j) { // 从后往前找比基准小的数
j--;
}
if(i < j) {
arr[i++] = arr[j];
}
while(arr[i] <= pivot && i < j) { // 从前往后找比基准大的数
i++;
}
if(i < j) {
arr[j--] = arr[i];
}
}
arr[i] = pivot;
quickSort(arr, left, i-1);
quickSort(arr, i+1, right);
return arr;
}
}
// example
let arr = [2, 10, 4, 1, 0, 9, 5 ,2];
console.log(quickSort(arr, 0 , arr.length-1));
法二:(借助两个空数组)参考某个元素值,将小于它的值,放到左数组中,大于它的值的元素就放到右数组中,然后递归进行上一次左右数组的操作,返回合并的数组就是已经排好顺序的数组了。
function quickSort(arr) {
if(arr.length<=1) {
return arr;
}
let leftArr = [];
let rightArr = [];
let q = arr[0];
for(let i = 1,l=arr.length; i<l; i++) {
if(arr[i]>q) {
rightArr.push(arr[i]);
}else{
leftArr.push(arr[i]);
}
}
return [].concat(quickSort(leftArr),[q],quickSort(rightArr));
}
堆排序
堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
- 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列
- 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列
计数排序
桶排序
思想:
- 设置固定空桶数
- 将数据放到对应的空桶中
- 将每个不为空的桶进行排序
- 拼接不为空的桶中的数据,得到结果
3、数组去重
1、利用ES6 Set
数据结构去重(ES6中最常用):
不考虑兼容性,这种去重的方法代码最少。这种方法还无法去掉“{}”空对象,后面的高阶方法会添加去掉重复“{}”的方法。简洁写法:[...new Set(arr)]
2、 利用for嵌套for,然后splice
去重(ES5中最常用):
双层循环,外层循环元素,内层循环时比较值。值相同时,则删去这个值。
3、利用indexOf
去重:
新建一个空的结果数组,for 循环原数组,判断结果数组是否存在当前元素,如果有相同的值则跳过,不相同则push进数组。
4、利用includes
(ES6新增的数组方法,为了完善indexOf方法不能识别NaN):
新建一个空的结果数组,for 循环原数组,判断结果数组是否存在当前元素,如果有相同的值则跳过,不相同则push进数组。
5、利用sort()
:
利用sort()排序方法,然后根据排序后的结果进行遍历及相邻元素比对。
6、利用filter
(语法:把传入的函数依次作用于每个元素,然后根据返回值是 true 还是false决定保留还是丢弃该元素。)
借助indexOf(),判断当前元素在原始数组中的第一个索引是否等于当前索引值
// 法一
function unique (arr) {
return Array.from(new Set(arr))
}
// 法二
[...new Set(arr)]
// 法三
function unique(arr){
for(var i=0; i<arr.length; i++){
for(var j=i+1; j<arr.length; j++){
if(arr[i]==arr[j]){ //第一个等同于第二个,splice方法删除第二个
arr.splice(j,1);
j--;
}
}
}
return arr;
}
4、二叉树相关
满二叉树、完全二叉树、平衡二叉树、二叉搜索树(排序树)、堆(大顶堆、小顶堆)
4.1.深度广度(层次)优先遍历
- 深度优先遍历(DFS):
从根节点出发,沿着左子树方向进行纵向遍历,直到找到叶子节点为止。然后回溯到前一个节点,进行右子树节点的遍历,直到遍历完所有可达节点为止。利用数据结构栈
,父节点入栈,父节点出栈,先右子节点入栈,后左子节点入栈。递归遍历全部节点。
- 广度优先遍历(层次遍历)(BFS):
根节点出发,在横向遍历二叉树层段节点的基础上纵向遍历二叉树的层次。利用数据结构队列
,父节点入队,父节点出队列,先左子节点入队,后右子节点入队。递归遍历全部节点。
function PrintFromTopToBottom(root)
{
// write code here
var arr=[];
var data=[];
if(root!=null){
arr.push(root);
}
while(arr.length!=0){
var node=arr.shift();
if(node.left!=null){
arr.push(node.left);
}
if(node.right!=null){
arr.push(node.right);
}
data.push(node.val);
}
return data;
}
4.2.二叉树前中后序遍历(深度)
前序遍历(前根遍历):根——>左——>右(借助栈)
// 前序遍历 非递归版
// 非递归版都需要用到栈
var preorderTraversal = function(root) {
if (!root) {
return []
}
let stack = []
let result = []
while (root || stack.length > 0) {
while (root) {
result.push(root.val)
stack.push(root)
root = root.left
}
if (stack.length > 0) {
root = stack.pop()
root = root.right
}
}
return result
};
// 前序遍历 递归版
var preorderTraversal = function(root) {
let result = []
let preorder = (root) => {
if (root) {
result.push(root.val)
preorderTraversal(root.left)
preorderTraversal(root.right)
}
}
return result
};
中序遍历(中根遍历):左——>根——>右(借助栈)
// 中序遍历 非递归版
// 非递归版都需要用到栈
var inorderTraversal = function(root) {
if (!root) {
return []
}
let stack = []
let result = []
while (root || stack.length > 0) {
while (root) {
stack.push(root)
root = root.left
}
if (stack.length > 0) {
root = stack.pop()
result.push(root.val)
root = root.right
}
}
return result
};
// 递归版
var inorderTraversal = function(root) {
let result = []
let inorder = (node) => {
if (!node) {
return
}
inorder(node.left);
result.push(node.val);
inorder(node.right);
}
inorder(root)
return result
};
后序遍历(后根遍历):左——>右——>根
// 递归版
var inorderTraversal = function(root) {
let result = []
let inorder = (node) => {
if (!node) {
return
}
inorder(node.left);
result.push(node.val);
inorder(node.right);
}
inorder(root)
return result
};
// 递归版本
var postorderTraversal = function(root) {
let result = []
let postorder = (node) => {
if (!node) {
return
}
postorder(node.left)
postorder(node.right)
result.push(node.val)
}
postorder(root)
return result
};
重建二叉树
已知前序和中序,求后序问题, 前序 ABDGCEFH 中序 DGBAECHF
解法:
- 根据前序序列第一个结点确定根结点
- 根据根结点在中序序列中的位置分割出左右两个子序列
- 根据左右子树的前序遍历和后续遍历
- 对左子树和右子树分别递归使用同样的方法继续分解
function TreeNode(x) {
this.val = x;
this.left = null;
this.right = null;
}
function reConstructBinaryTree(pre, vin)
{
// write code here
var result =null;
if(pre.length>1){
var root = pre[0];
var vinRootIndex = vin.indexOf(root);
var vinLeft = vin.slice(0,vinRootIndex);
var vinRight = vin.slice(vinRootIndex+1,vin.length);
pre.shift();
var preLeft = pre.slice(0,vinLeft.length);
var preRight = pre.slice(vinLeft.length,pre.length);
result={
val:root,
left:reConstructBinaryTree(preLeft,vinLeft),
right:reConstructBinaryTree(preRight,vinRight)
}
}else if(pre.length ===1){
result= {
val :pre[0],
left:null,
right:null
}
}
return result;
}
5、数组拍平
function flat(a=[],result=[]){
a.forEach((item)=>{
console.log(Object.prototype.toString.call(item))
// 判断是否是数组
if(Object.prototype.toString.call(item)==='[object Array]'){
result=result.concat(flat(item,[]));
}else{
result.push(item)
}
})
return result;
}
let a = [1,2,3, [1,2,[1.4], [1,2,3]]]
console.log(flat(a)) // 输出结果 [ 1, 2, 3, 1, 2, 1.4, 1, 2, 3 ]
6、走台阶问题(递归)
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法
核心思想:
f(n) = f(n-1) + f(n-2)
f(1) = 1,f(2) = 2
function f(n){
if(n === 1) return 1;
if(n === 2) return 2;
return f(n-1) + f(n-2)
}
进阶版:
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
由数学归纳法得:
| 1 ,(n=0 )
f(n) = | 1 ,(n=1 )
| 2*f(n-1),(n>=2)
7、判断一个字符串是否是回文?
// 1234321类似这样就是回文,翻转还是原来的值
function checkPalindrom (str) {
return str == str.split('').reverse().join();
}
8、统计一个字符串出现最多的字母
借助一个对象统计
function findMaxDuplicateChar(str) {
if(str.length == 1) {
return str;
}
let charObj = {};
for(let i=0;i<str.length;i++) {
if(!charObj[str.charAt(i)]) {
charObj[str.charAt(i)] = 1;
}else{
charObj[str.charAt(i)] += 1;
}
}
let maxChar = '',
maxValue = 1;
for(var k in charObj) {
if(charObj[k] >= maxValue) {
maxChar = k;
maxValue = charObj[k];
}
}
return maxChar;
}
9、不借助临时变量,进行两个整数的交换
function swap(a , b) {
b = b - a;
a = a + b;
b = a - b;
return [a,b];
}
10、斐波那契数列
借助递归:f(n) = f(n-1) + f(n-2)
function getFibonacci (n) {
var fibarr = [];
var i = 0;
while(i<n) {
if(i<=1) {
fibarr.push(i);
}else{
fibarr.push(fibarr[i-1] + fibarr[i-2])
}
i++;
}
return fibarr;
}
矩形覆盖问题
我们可以用21的小矩形横着或者竖着去覆盖更大的矩形。请问用n个21的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?
result[i] = result[i-1]+result[i-2]
function rectCover(number)
{
// write code here
if(number == 0){
return 0;
}
var result = [1,1];
for(var i=2;i<=number;i++){
result[i] = result[i-1]+result[i-2];
}
return result[number];
}
11、输入两棵二叉树A,B,判断B是不是A的子结构
- 首先判断两个二叉树的根节点是否相等,如果相等的话就去递归判断左右子树是否相等;
- 如果根节点不相等,则根节点的左节点重新作为根节点判断是否等于B的根节点,这个也是通过递归实现,如果在左节点中找不到和B根节点相等的值,则与此类似递归调用右子树,直到找到一个和B根节点相等的节点或者遍历结束
/* function TreeNode(x) {
this.val = x;
this.left = null;
this.right = null;
} */
function HasSubtree(pRoot1, pRoot2) {
if (!pRoot1 || !pRoot2) {
return false;
}
return isSubtree(pRoot1, pRoot2) || HasSubtree(pRoot1.left, pRoot2) || HasSubtree(pRoot1.right, pRoot2);
}
function isSubtree(root1, root2) {
if (!root2) {
return true;
}
if (!root1) {
return false;
}
if (root1.val == root2.val) {
return isSubtree(root1.left, root2.left) &&
isSubtree(root1.right, root2.right);
} else {
return false;
}
}
12、输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序
参考文章
借用一个辅助栈,遍历压栈顺序,先将第一个放入栈中,这里是1,然后判断栈顶元素是不是出栈顺序的第一个元素,这里是4,很显然1≠4,所以我们继续压栈,直到相等以后开始出栈。出栈一个元素,则将出栈顺序向后移动一位,直到不相等,这样循环等压栈顺序遍历完成,如果辅助栈还不为空,说明弹出序列不是该栈的弹出顺序
function IsPopOrder(pushV, popV)
{
// write code here
var stack = []
var idx = 0;
for(var i = 0;i< pushV.length;i++){
stack.push(pushV[i])
while(stack.length&& stack[stack.length-1] == popV[idx]){
stack.pop()
idx++;
}
}
return stack.length == 0;
}
13、字符串的全排列
- 将第一个字符与自己本身还与其他的字符进行交换
- 在第一步每种情况的基础上,保持第一个字符不变,将剩余几位按照第一步一样排列
- 到保存到只剩下一个字符没有交换后加上前面的不变的字符作为输出结果
14、字符串的比较
题目:输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。
思路:重新定义两个数的大小,排好序之后按顺序将字符串拼接起来即可。
- 先将所有的数字转换成字符串,想要比较字符串a和字符串b的大小,就是比较拼接之后的字符串ab和字符串ba的大小。如果ab < ba,则a<b。
- 定义好这个规则之后,就可以用
array.sort(compare)
函数进行排序,最终将所有的字符串拼接起来。
function compare(num1,num2){
var a = num1 + "" + num2;
var b = num2 + "" + num1;
for(var i=0;i<a.length;i++){
if(a.charAt(i) > b.charAt(i)){
return 1;
}
if(a.charAt(i) < b.charAt(i)){
return -1;
}
}
return 1;
}
function PrintMinNumber(numbers) {
numbers.sort(compare);
var result = "";
for (var i = 0; i < numbers.length; i++) {
result = result + numbers[i];
}
return result;
}
15、给定一个字符串,找出不含有重复字符的最长子串的长度
var lengthOfLongestSubstring = function(s){
var str = '';//存放当前最大无重复项字符串
var len = 0;//存放当前当前最大无重复项字符串长度
for(var i=0;i<s.length;i++){
var char = s.charAt(i);
var index = str.indexOf(char);
if(index === -1){ //遍历s将元素挨个放入到str中,每放入一次判断str中是否有重复项
str += char;
len = len<str.length?str.length:len;//记录最大长度项,每次重新记录str进行判断,是否大于上次一次最大项
}
else{
str = str.substr(index + 1) + char; //从当前重复项开始重新记录str
}
}
return len;
}
16、动态规划:连续子数组最大和
function FindGreatestSumOfSubArray(array)
{
if(array.length <= 0) return 0;
var max = array[0];//包含array[i]的连续数组最大值
var res = array[0];//记录当前所有子数组的和的最大值
for(var i = 1;i<array.length;i++){
max=Math.max(max+array[i], array[i]);
res=Math.max(max, res);
}
return res
}
17、和为s的连续正数序列
双指针技术,就是相当于有一个窗口,窗口的左右两边就是两个指针,两个窗口都是从左边出发,不是两边夹逼。另外,当小于目标数时high++;大于目标数时low++,如果是high–,那么你仔细想想,你的窗口还怎么往后移动,整个结果在第一次大于目标数之后就不会往后移动,相反,而是在在这个low和high之间夹逼试探,最终啥都找不到或者只能找到一个。(和tcp滑动窗口类似)
时间复杂度基本是接近O(n)了
function FindContinuousSequence(sum){
let start = 1; // 开始指针
let end = 2; // 结束指针
let sumTemp = 0; // 窗口中所有数的总和
let array = [1,2]; // 窗口中包含的数
let ans = []; // 窗口中总和为100的所有数
if (sum < 3) return [];
while (start < end) {
//由于是连续的,差为1的一个序列,那么求和公式是(a0+an)*n/2
sumTemp = (start + end) * (end - start + 1) / 2;
if(sumTemp === sum) {
// 如果当前窗口内的值之和等于sum,那么左边窗口和右边窗口都右移一下
ans.push(array.concat());
array.shift();
start++;
end++;
array.push(end);
}else if(sumTemp > sum) {
//如果当前窗口内的值之和大于sum,那么左边窗口右移一下
array.shift();
start++;
} else {
//如果当前窗口内的值之和小于sum,那么右边窗口右移一下
end++;
array.push(end);
}
}
return ans;
}
18、和为s的两个数字(夹逼法)
数列满足递增,设两个头尾两个指针i和j,
若ai + aj == sum,就是答案(相差越远乘积越小)
若ai + aj > sum,j –
若ai + aj < sum,i ++
时间复杂度:O(n)
function FindNumbersWithSum(array, sum)
{
// write code here
if(array.length < 2)
return [];
var start = 0,
end = array.length-1;
while(start < end){
if(array[start]+array[end] < sum){
start++;
}else if(array[start]+array[end] > sum){
end--;
}else{
return [array[start],array[end]];
}
}
return [];
}
Promise实现
class Promise { //创建一个Promise类
constructor(executor) {
this.status = 'pending'; //初始默认状态为pending
this.value = undefined; //默认赋值为undefined
this.reason = undefined; //默认赋值为undefined
this.successStore = []; //定义一个存放成功函数的数组
this.failStore = []; //定义一个存放失败函数的数组
let resolve = (value) => {
if (this.status === 'pending') { //只有状态为pending才能转换状态
this.value = value; //将传递进来的的值赋给value保存
this.status = 'resolved'; //将状态设置成resolved
this.successStore.forEach(fnc => fnc()); //一次执行数组中的成功函数
}
}
let reject = (reason) => {
if (this.status === 'pending') { //只有状态为pending才能转换状态
this.reason = reason; //将传递进来的失败原因赋给reason保存
this.status = 'rejected'; //将状态设置成rejected
this.failStore.forEach(fnc => fnc()) //依次执行数组中的失败函数
}
}
//executor(resolve, reject);
try {
executor(resolve, reject);
} catch (e) {
reject(e);//如果发生错误,将错误放入reject中
}
//默认执行executor
}
then(onFulfilled, onRejected) { //等同于es5的Promise.prototype.then 当调用then的时候,根据状态,来执行不同的函数
if (this.status === 'resolved') { //如果状态是resolved
onFulfilled(this.value); //执行成功的resolve,并将成功后的值传递过去
}
if (this.status === 'rejected') { //如果状态是rejected
onRejected(this.reason); //执行失败的reject,并将失败原因传递过去
}
if (this.status === 'pending') { //此处增加一种状态判断
this.successStore.push(() => { //当状态为pending时将成功的函数存放到数组里
onFulfilled(this.value);
})
this.failStore.push(() => { //当状态为pending时将失败的函数存放到数组中
onRejected(this.reason);
})
}
}
}
module.exports = Promise; //将Promise导出
防抖
- 非立即执行版:
function debounce(fn, wait) {
var timeout;
return function() {
if(timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(fn, wait);
}
}
- 立即执行版:
function debounce(func,wait) {
let timeout;
return function () {
let context = this; // 应为我们要注意的是setTimeout()函数中作用域是全局的,也就是setTimeout中的this指的是window,我们需要的上下文是我们所绑定的Dom对象
let args = arguments;
if (timeout) clearTimeout(timeout);
let callNow = !timeout;
timeout = setTimeout(() => {
timeout = null;
}, wait)
if (callNow) func.apply(context, args)
}
}
节流
- 时间戳版(立即执行):
function throttle(func, wait) {
let previous = 0;
return function() {
let now = Date.now(); // 当前时间
let context = this;
let args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now; // 上次执行完的时间(可能是忽略了函数自身执行时间)
}
}
}
- 定时器版:
function throttle(func, wait) {
let timeout;
return function() {
let context = this;
let args = arguments;
if (!timeout) {
timeout = setTimeout(() => {
func.apply(context, args) // 函数执行,此时定时器还没清空
timeout = null; // 函数执行完之后清空定时器
}, wait)
}
}
}
深拷贝
function deepCopy(object) {
// // 只拷贝对象
if (!object || typeof object !== "object") return;
// // 根据 object 的类型判断是新建一个数组还是对象
let newObject = Array.isArray(object) ? [] : {};
// // 遍历 object,并且判断是 object 的属性才拷贝
for (let key in object) {
if (object.hasOwnProperty(key)) {
newObject[key] = typeof object[key] === "object" ? deepCopy(object[key]) : object[key];
}
}
return newObject;
}
call、apply、bind实现
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;
}
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;
}
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));
}
}