手写系列
Willem Zhang Lv5

手写系列

1. 数据类型判断

typeof()不能准确判断数据类型 比如Array、Date

思路:使用Object的原型的toString方法得到数据类型,然后通过slice和tolowercase去掉多余的部分整理成都是小写的形式。
subString也可以用,但是subString不能识别-1,所以只能通过求长度的方法得到右边的”]”索引,另外subStr即将被弃用。

1
2
3
4
5
6
7
8
9
10
11
12
13
let typeOf = function(obj){Object.prototype.toString.call(obj).slice(8, -1).toLowerCase()}
// 返回值都是undefined 为什么?因为没有返回值。

let typeOf = function(obj){
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase()
}

function typeOf(obj) {
// Object.prototype.toString.call(obj).slice(8, -1).toLowerCase()
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase() //需要reutrn
}

let typeOf = (obj) => (Object.prototype.toString.call(obj).slice(8, -1).toLowerCase())
1
2
3
(function(){

})()

2. 继承

  1. 原型链继承
    (实现:实例属性定义在父类构造函数中,方法定义在父类原型中;子类原型指向父类的实例
    缺点:原型中的引用类型实例属性会被所有子类实例共享,基本类型不会是因为复制的是值,而引用类型被复制的是地址。)
  2. 借用构造函数继承
    (实现:实例属性和方法都定义在父类构造函数中;子类原型指向父类实例
    缺点:每次创建子类实例都会导致方法被创建一次。
    但是解决了原型链继承的父类实例引用类型被子类实例共享的问题以及传参问题,传参问题?)
  3. 组合继承
    (以上两种方法的结合体,原型链继承是方法写在父类原型中,借用构造函数是借用父类的构造函数作为函数使用。原型链继承只用了原型链,借用构造函数只借用了构造函数。而在组合继承中将两种方法结合起来,给父类原型添加方法来继承方法,借用构造函数来继承实例属性。
    那么给父类赋予属性是什么继承??和原型链继承效果一样而且使得父类原型多了多余的属性。
    所以原型链继承没有将实例属性也写到父类实例中,因为效果相同,反而改变父类实例更多。
    原型链继承可以将属性和方法全都写入父类原型,但是发现属性写到构造函数效果相同,所以写到了构造函数,但是写到构造函数中还是不能使得每个子类实例获得独立的引用变量,于是子类构造函数需要借用父类构造函数声明和父类一样的子类实例属性;而借用构造函数继承会使得方法复制多份,所以将方法写到父类原型中。
    缺点:调用两次父类构造函数)
  4. 原型式继承
    (无实例属性的临时构造函数根据父类原型创造一个无实例属性的实例作为子类原型)
  5. 寄生
    (原型式继承中得到的父类实例基础上给作为子类原型的无实例属性父类实例增加属性,然后将这个增强的父类实例作为子类原型)
  6. 寄生组合继承
    (为解决组合继承每次实例化子类时调用两次父类构造函数的问题,使用寄生组合继承。
    寄生组合继承通过借用构造函数继承实例属性,通过原型链连接增强的父类原型继承方法)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// 继承想要做的是继承父类的属性和方法 不只方法,属性也要继承 只是原型链的继承方式继承属性有瑕疵

// 1. 原型链继承
function SuperType(){
this.type = 'typeA';
}

SuperType.prototype.method = function() {
// 原型链继承 意思是子类只通过原型链继承父类的属性和方法 通过给父类原型添加方法使子类实例继承到方法 通过实例化父类作为子类原型继承到属性 子类实例通过原型链获取到原型上的属性和方法,属性从父类的实例上获取,方法从父类的原型上获取。或许方法也可以定义到父类的构造函数里,属性也可以定义到父类的原型中,以上两种造成的结果只是,获取方法和属性只是都从父类实例取得或者是都从父类原型取得,但是基本的原型链继承和以上两种变体都是从原型链上面的原型(可能是子类原型「父类实例」,可能是父类原型)找到方法和属性,而子类实例自己没有自己的属性和方法。

// 借用构造函数继承 意思是子类只通过借用构造函数来继承父类的属性和方法 父类构造函数中需要同时有属性和方法,而不修改父类的原型 这样子类在实例化的时候借用父类的构造函数中对属性和方法的设置,子类实例就拥有了自己的属性和方法,而不是从原型链上面找到的原型的属性和方法。
}

function SubType(){
}

SubType.prototype = new SuperType(); //实例化父类时已经继承了父类的属性,只是引用类型子类实例不能独立拥有 所以叫原型链继承

new SubType();
-----------------------------------------------------------------------------
// 2. 借用构造函数继承
function SuperType() {
this.type = 'typeA';
this.func = function(){}; //属性和方法定义在构造函数 使得子类实例借用时这个构造函数的内容时可以获得自己独立的属性和方法,而不是从原型链上获取原型的属性和方法,这种方法使得属性独立(想要看到的),但是方法复制的多份(不想看到的)。

// 想要实现的是属性各实例独立,但是方法各实例共用。
}

function SubType(){
SuperType(this); //借用构造函数
}


new SubType();
-----------------------------------------------------------------------------
// 3. 组合继承
function SuperType() {
this.type = 'typeA'; //只写属性
}

SuperType.prototype.method = function() {}; //在原型上写方法用来被子类实例继承

function SubType() {
SuperType(this); //借用构造函数继承属性
}

SubType.prototype = new SuperType(); //子类原型等于父类实例来构造原型链,以使得子类实例能够通过原型链继承父类原型上的方法
SubType.prototype.constructor = SubType; //指定构造函数

// 缺点:调用了两次父类构造函数
-----------------------------------------------------------------------------
// 4. 原型式继承
function object(o) { //返回一个只有__proto__属性的对象,隐式原型指向传入的对象
function F() {} //临时无实例属性构造函数
F.prototype = o //将父类原型当作素体
return new F()
}

let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};

let anotherPerson = object(person); //anotherPerson的原型是person对象 生成了一个只有__proto__属性的新对象,这个对象的所有方法与属性都在它的原型身上

Object.create()原生方法实现对原型的继承

// 5. 寄生式继承
function Parasitic(original) {
let clone = object(original);
clone.sayHi = function() { //原型式继承基础上对对象进行增强 增强的方法在新对象身上 别的方法在素体对象上找
alert("hi");
};
return clone;
}

let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};

let anotherPerson = Parasitic(person);
anotherPerson.sayHi();



// 6. 寄生组合继承

// 通过寄生式继承超类原型指定为子类原型(跳过超类实例化,减少一次超类调用)来继承超类方法,通过借用构造函数继承属性

// 将方法放在原型上,通过原型链继承方法

function SuperType() {
this.type = 'typeA';
}

SuperType.prototype.method = function() {}; // 定义可继承的方法


function SubType() {
SuperType(this); // 借用构造函数继承属性
}

// 此时如果让子类原型指向超类的实例,则为组合继承。但是这里让子类原型指向寄生式继承超类原型的实例,越过了超类实例化。组合继承为原型链和借用构造函数;寄生式组合继承为原型链和借用构造函数,不过这里的原型链在构建时有些区别,子类的原型没有指向超类构造函数构造的实例,而是指向了对父类原型进行寄生式继承得到的实例,使得子类原型没有父类构造函数的实例属性。 寄生式继承为原型式继承的基础上进行增强,加上constructor属性。

function inheritPrototype(SubType, SuperType) {
let prototype = object(SuperType.prototype) //原型式继承超类原型
prototype.constructor = SubType //增强 //合起来为寄生式继承
SubType.prototype = prototype //指定子类原型为寄生式继承父类原型的实例
}

inheritPrototype(SubType,SuperType);

new SubType();

// 7. class实现继承

class SuperType {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
class SubType extends SuperType {
constructor(name, age) {
super(name)
this.age = age
}
}


  1. 数组去重
1
2
3
4
5
6
7
8
9
10
11
12
// ES5 indexOf()方法返回第一次出现元素的位置,filter使得只有索引值为第一个索引值的同值元素才能输出 即同一个值只能输出一次 也就是说只有第一次出现的值才能够输出 如果item的index和indexOf(item)的值不一样,说明这个值不是第一次出现,返回false,则不会输出
function unique(arr) {
var res = arr.filter(function(item, index, array) {
return array.indexOf(item) === index
})
return res
}

// ES6
let unique = function(arr) { return [...new Set(arr)]};

var unique = arr => [...new Set(arr)]
  1. 数组扁平化
1
2
3
4
5
6
7
8
[1, [2, [3]]].flat(2)  // [1, 2, 3] 原生方法

function flatten(arr) {
while (arr.some(item => Array.isArray(item))) { // 判断语句为数组里是否还有数组,没有数组则停止循环
arr = [].concat(...arr); // concat的作用是将值或数组添加到拥有concat的实例身上,当括号里是数组时,该数组会展开然后放入前面的数组里。
}
return arr;
}
  1. 字符串模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    function render(template, data) {
    const reg = /\{\{(\w+)\}\}/
    if (reg.test(template)) {
    const prop = reg.exec(template)[1]; // 查找模板中第一个变量
    template = template.replace(reg, data[prop]); // 用data对象中对应的变量替换模板的{{prop}}
    return render(template, data); // 两个动作:1. 调用自己递归继续渲染模板 2. 返回渲染后的结果(这个必须返回) 同时,这一行不会同时执行,而是会先执行render函数,层层递归,直到最后一个render有了返回值,然后从深层依次往浅层return。 最先从这里声明return,所以最后在这里return最后的结果。
    }
    return template; // 注意:!! 这个返回的template不会直接给控制台,而是给调用它的函数,最后一次render调用了它。所以它会返回给render
    }

    let template = '我是{{name}},年龄{{age}},性别{{sex}}';

    let person = {
    name: '布兰',
    age: 12
    }
    render(template, person); // 我是布兰,年龄12,性别undefined

  2. 图片懒加载

img.getBoundingClientRect()

img.dataset.src

  1. 防抖

高频事件 触发N秒后执行一次 N秒钟内再次触发则重新计时

1
2
3
4
5
6
7
8
9
10
11
function debounce(func, wait) {
const timeout = null;
return function() {
let context = this;
let args = arguments;
clearTimeout(timeout); //停掉之前的计时
timeout = setTimeout(function() { //设置新计时 将这两行代码(1. 停掉之前的计时 2. 然后设置一个新的计时)看成一个整体,就是重新计时功能的实现。 重新计时:之前的不算,从现在开始。之前的计时去掉,然后设置一个新的计时。
func.apply(context, args);
}, wait)
}
}

有即时执行功能的防抖
一开始触发一次,N秒后不再触发,否则会导致点击一次触发两次。
重新计时有两步:1. 停掉之前的 2. 设置新开的

即时执行:一开始就触发,之后的N秒内点击不能触发,重新计算不能触发的时间
从n秒后执行到n秒后才能执行。
设置setTimeout的内容为n秒后将timeout置为null,唯有时间才能将timeout值为null,所以设置timeout值为null时能够触发函数,当计时器不为null时,说明计时器还没有计时结束,此时不能触发函数。

clearTimeout只是将计时器暂停了,计时器还存在,timout变量还存有计时器的id。

timeout有值不执行,为null则执行,每当点击一次,则计时器重新计时,timeout变成null的事件就往后延迟,从而不能再次使事件发生。

1
2
3
4
5
6
7
8
9
10
11
12
13
function debounce(fn, delay, immediate = true) {
let timer = null
// 给计时器命名是为了将来能够停掉它,每次执行都会生成一个新的timer
return function (...args) {
immediate && !timer && fn.apply(this, args) //是否执行的逻辑 timer为null才可执行 timer计时器计时完成才被设置为null 计时过程中都不可执行操作
if (timer) clearTimeout(timer)
timer = setTimeout(() => { //和上面一行共同构成重新计时功能
timer = null // 这行代码只在即时执行时起关键作用 不即时的话对运行结果没有作用 只要有timer的id就能在计时器运行时停掉它,而计时器运行结束就不需要停掉它也不不需要知道它的id了,所以运行结束后将其值设置为null也没有影响,只不过回归了初始状态,在下次点击的时候if也不会被触发。
// 运行时需要id停掉计时器,运行结束后,timer值随意处置
!immediate && fn.apply(this, args)
}, delay)
}
}
  1. 节流
1
2
3
4
5
6
7
8
9
10
11
12
13
// timer = null为初始化,函数执行后返回一个闭包函数,当timer为null时执行if里面的函数给timer赋一个计时器并执行一次函数,赋值之后if里面的语句将不能被执行,这个计时器在delay时间后将timer变为null以使得可以再次触发if里面的函数,来达到节流的效果。
function throttle(fn, delay, immediate = true) {
let timer = null
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
timer = null
!immediate && fn.apply(this, args)
}, delay)
immediate && fn.apply(this, args)
}
}
}

ref

36

传参问题
意思是每个子类实例不能传参,而给作为子类原型的父类实例传参又不能保证每个子类实例有独立的实例属性(引用类型的情况)

一个合格的中级前端工程师需要掌握的 28 个 JavaScript 技巧

防抖节流

  • Post title:手写系列
  • Post author:Willem Zhang
  • Create time:2021-11-21 22:42:23
  • Post link:https://ataraxia.top/2021/11/21/手写系列/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.
 Comments