五一假期安排
- 防抖和节流函数
- new 构造函数实例
- 手写apply/bind/call
- 单例模式以及vue单组件
- 箭头函数
- 继承
函数防抖和节流
都阔以用于持续触发函数的优化中,防抖是,触发事件后n秒内,函数只能执行一次。如果在N秒内又重新触发,则需要重新计时开始。或者简洁点:连续触发的时候,只会执行一次。在停止N秒之后才能继续执行,典型的案例就是防止多次提交的按钮
而节流呢,是每间隔N秒,只执行一次。就像水龙头里的水,节流只能减缓水流,但事件依然会执行。频率变少了。典型案例是滚动scroll/resize事件
两者最大的区别就是节流是依然执行,可用于滚动事件。而防抖,如果一直在触发中,只有停下来的时候才会执行一次。
1 | /** |
防抖函数,主要是采用异步线程setTimeout进行延时执行,立即执行,是在触发事件的开始的时候就立即执行。而非立即执行版的防抖,就是执行完N秒之后,不触发事件才会执行
箭头函数没有自己的arguments,但是阔以通过命名参数的形式或者rest参数的形式传参
示例
1 | // 防抖 非立即执行 |
addEventListener的第二个参数实际上是debounce函数里return回的方法,let timeout = null 这行代码只在addEventListener的时候执行了一次 触发事件的时候不会执行,那么每次触发scroll事件的时候都会清除上次的延时器同时记录一个新的延时器,当scroll事件停止触发后最后一次记录的延时器不会被清除可以延时执行,这是debounce函数的原理
1 | /** |
节流最大的特点就是减少事件的频率,可能由1毫秒到1000毫秒才能触发事件。事件依然会执行,频率变少。时间戳版和定时器版各有特色,都是满足一个假设条件才能执行事件,执行事件都是用apply绑定
示例
1 | // 节流 定时器版 |
new 构造函数和模拟实现
如何理解执行上下文
context主要指代码执行环境,分为
- 全局执行环境
- 函数执行环境
- eval执行环境
每一段js代码执行,都会先创建一个上下文环境
作用域
作用域其实可理解为该上下文中声明的 变量和声明的作用范围。可分为 块级作用域 和 函数作用域
如何理解作用域链
我们知道,我们可以在执行上下文中访问到父级甚至全局的变量,这便是作用域链的功劳。作用域链可以理解为一组对象列表,包含 父级和自身的变量对象,因此我们便能通过作用域链访问到父级里声明的变量或者函数。
由两部分组成:
- [[scope]]属性: 指向父级变量对象和作用域链,也就是包含了父级的[[scope]]和AO
- AO: 自身活动对象
如此 [[scope]]包含[[scope]],便自上而下形成一条 链式作用域。
从当前环境向父级一层一层查找变量的过程
如何理解原型链
前期: 每个构造函数都有一个prototype属性,每个实例对象都有一个__proto__
对象,而这个对象指向构造函数的prototype属性。
当我们访问实例对象的属性或者方法时,首先从自身构造函数中查找,如果没有就通过proto去原型上查找,这个查找的过程我们称之为原型链。
new 做了哪些操作
1.创建了一个新对象
2.这个对象也就是构造函数中的this,阔以访问挂载在this上的任意属性
3.这个对象还能访问构造函数原型上的属性,需要将对象与构造函数链接起来
4.默认返回this,如果手动定义返回原始值不影响,返回对象需要正常处理
手动实现一个new 操作符
1 | /** |
create说明
1.接受构造函数和其他参数
2.创建obj对象,同时要继承构造函数的原型链上的属性和方法,所以我们通过 Object.create(Con.prototype)实现,或者通过
setPrototypeOf 将两者联系起来。这段代码等同于 obj.proto = Con.prototype,即继承构造函数的原型链上的属性和方法有三种
1 | // first |
3.生成新的对象会绑定到构造函数上this对象上,并且传入剩余的参数即
1 | // Con方法绑定this对象,这里的this即obj |
4.返回值处理
用法
1 | private mounted() { |
模拟new简洁版
1 | function Create(Con, ...args) { |
apply && call && bind()手动实现
call 用法
call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。
说起来有点拗口,就是指定this值,调用某个函数
举例子:
1 | const bar: any = function(this: any, name: string, age: number) { |
注意两点:
- call 改变了this的指向,指向到 foo
- bar 函数执行了
模拟实现
假设这样
1 | const foo = { |
但是这样多了一个属性,但是阔以删除,大体分为这几步,简单版的myCall
1 | function myCall(con) { |
使用
1 | const bar: any = function(this: any, name: string, age: number) { |
传参
call 函数还能给定参数执行函数,直接上
1 | mockCall: function (con, ...args) { |
返回
bar函数阔以有返回值的
1 | const bar: any = function(this: any, name: string, age: number) { |
所以最终版的就是
1 | mockCall: function (con, ...args) { |
bind实现
1 | /** |
Vue手动挂载组件
本文主要有以下内容
- Vue.extend()
- 单例模式
- Vue.use() 和 Vue.prototype.myFunction
挂载组件步骤
在一些需求中,手动挂载组件能够让我们实现起来更加优雅。比如一个弹窗组件,最理想的用法是通过命令式调用,就像 elementUI 的 this.$message
vue.extend()
使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。预设了部分选项的vue实例构造器,返回一个组件构造器,用来生成组件,可以在实例上扩展方法,从而使用更灵活
1 | <template> |
结果如下
1 | <p>Walter White aka Heisenberg</p> |
Vue.extend创建的是一个Vue组件构造器,而不是一个具体的组件实例;里面预设了很多vue实例选项
单例模式
先看一个简单的例子getSingle
1 | const Anima: any = function(this: any, name: string) { |
- 使用闭包封装了instance私有变量并返回一个函数
- 利用 || 语法判断如果instance不存在则执行后者的实例化Anima方法,存在则直接返回instance,确保了只存在一个弹框实例
实现方式:使用一个变量存储类实例对象(值初始为 null/undefined )。进行类实例化时,判断类实例对象是否存在,存在则返回该实例,不存在则创建类实例后返回。多次调用类生成实例方法,返回同一个实例对象。
构建属于自己的封装组件
用法,首先main.ts中全局引入
1 | import Toast from './components/Toast/index'; |
具体的组件用法
1 | private showSuccess() { |
来看看如何实现的,主要思路是这样的
1 | import toast from './toast.vue' |
如果想在main.ts中Vue.use()引入的话,导出一个传递参数的install
方法即可
1 | ['success', 'error', 'info'].forEach((type: any) => { |
这样在main.ts中直接引入即可
1 | import Toast from './components/Toast' |
为啥需要用use才能用,源码看了下,在GlobalAPI下
1 | export function initUse (Vue: GlobalAPI) { |
1 | /** |
关于Vue.use(plugin)和Vue.prototype.$plugin = Plugin的区别
Vue.use(): 插件必须是一个对象,拥有install方法的对象,初始化插件必须有Vue.use()引入。同一个插件多次使用Vue.use()也只会运行一次。且vue.use()必须在new Vue()之前使用。
Vue.prototype.$plugin = Plugin: 在Vue组件构造器函数的原型上增加一个方法,运用的是函数原型的特性,即函数原型上的属性和方法,实例都能共享
箭头函数
基本用法
与变量解构结合,并隐式返回
1
2
3
4
5
6const Test1 = ({value, num}: any) => ({total: value * num})
const res = Test1({
value: 100,
num: 10
})
console.log('res', res);
与普通函数的区别
没有this
箭头函数没有 this,所以需要通过查找作用域链来确定 this 的值。因为箭头函数没有 this,所以也不能用 call()、apply()、bind() 这些方法改变 this 的指向
没有arguments
但命名参数或者 rest 参数的形式访问参数:
不能通过 new 关键字调用
当通过 new 调用函数时,执行 [[Construct]]
方法,创建一个实例对象,然后再执行函数体,将 this 绑定到实例上。
箭头函数并没有 [[Construct]]
方法,不能被用作构造函数,如果通过 new 的方式调用,会报错。
于是箭头函数也不存在 prototype 这个属性。
无new.target
因为不能使用 new 调用,所以也没有 new.target 值
哪些场景下不能使用箭头函数
借鉴知乎大佬王仕军的文章,原文请移步什么时候你不能使用箭头函数?
定义对象里的方法
在一个对象上,定义一个指向函数的属性,当方法被调用时,方法内的this指向方法所属的对象
定义字面量方法
1
2
3
4
5
6
7
8
9
10const calculator = {
array: [1, 2, 3],
sum: () => {
console.log(this === window); // => true
return this.array.reduce((result, item) => result + item);
}
};
console.log(this === window); // => true
// Throws "TypeError: Cannot read property 'reduce' of undefined"
calculator.sum();calculator.sum 使用箭头函数来定义,但是调用的时候会抛出 TypeError,因为运行时 this.array 是未定义的,调用 calculator.sum 的时候,执行上下文里面的 this 仍然指向的是 window,原因是箭头函数把函数上下文绑定到了 window 上,this.array 等价于 window.array,显然后者是未定义的。
改造普通函数1
2
3
4
5
6
7
8const calculator = {
array: [1, 2, 3],
sum() {
console.log(this === calculator); // => true
return this.array.reduce((result, item) => result + item);
}
};
calculator.sum(); // => 6定义原型方法
1
2
3
4
5
6
7
8
9function Person(this: any, name: any, game: string = 'cf') {
this.name = name
this.habits = game
}
Person.prototype.age = 30;
Person.prototype.sayName = () => {
console.log('sayName: i am', this.name);
}
this.person1 = new (Person as any)('cpp')使用传统的函数表达式就能解决问题
定义事件回调函数
this 是 JS 中很强大的特性,可以通过多种方式改变函数执行上下文,JS 内部也有几种不同的默认上下文指向,但普适的规则是在谁上面调用函数 this 就指向谁,这样代码理解起来也很自然,读起来就像在说,某个对象上正在发生某件事情。
但是,箭头函数在声明的时候就绑定了执行上下文,要动态改变上下文是不可能的,在需要动态上下文的时候它的弊端就凸显出来
1 | const button = document.getElementById('myButton'); |
修正后
1 | const button = document.getElementById('myButton'); |
定义构造函数
构造新的 Person 实例时,JS 引擎抛了错误,tslint也直接给出了提示An arrow function cannot have a 'this' parameter
,还有Property 'name' does not exist on type 'Home'
1 | const Person = (this: any, name: any, game: string = 'cf') => { |
改成普通函数即可
继承
借助原型链实现继承
子类Child的原型属性等价于父类的一个实例,并且传参
1 | private extendPrototype() { |
子类能继承父类的属性和方法
缺点:
- 创建child类的时候,不能像Parent传参
- 子类创建的实例所在的原型链上的属性共享
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23private extendPrototype() {
const Parent: any = function(this: any, name: string, arr: any) {
this.name = name
this.arr = arr
}
Parent.prototype.sayName = function() {
console.log(this.name, '——this.name——')
}
const Child: any = function (this: any, name: string, age: number) {
this.age = age
}
Child.prototype = new Parent('chendapeng', [1,2,3])
const p1 = new Parent('wmh', [1,2,3]);
const c1 = new Child('cpp', 30);
c1.arr.push(4)
const c2 = new Child('pp', 99)
console.log('p1,', p1.arr); [1,2,3]
console.log('c1,', c1.arr); // [1,2,3,4]
console.log('c2,', c2.arr); // [1,2,3,4]
// p1.sayName() // wmh ——this.name——
// c1.sayName() // c1.sayName is not a function
}
子类实例c1和c2上的arr属性都是从父类继承过来的,改了c1.arr属性,c2.arr也发生了变化思考题
1
2
3
4
5
6
7
8
9
10var A= {n: 4399}
var B = function() {this.n = 9999};
var C = function() {var n = 8888};
B.prototype = A;
C.prototype = A
var b = new B()
var c = new C()
A.n ++;
console.log(b.n) // 9999
console.log(c.n) // 4400
借助构造函数继承(经典继承)
先看代码,Parent 是父类,Child 是子类。通过 Parent1.call(this, name) 改变了 this 指向,使子类继承了父类的属性,即 Child 也有了 name 属性。
1 | private extendConstructor() { |
缺点
- 这种方式不能继承父类原型链上的属性,只能继承在父类显式声明的属性
- 方法都在构造函数中定义,每次创建实例都会创建一遍方法。
组合继承
优点:融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式
1 | private extendTwo() { |
原型式继承
ES5 Object.create()
的模拟实现,将传入的对象作为创建的对象的原型。根据自己的业务需求,定义自己的原型对象。
1
2
3
4
5funtion createObj(o) {
function F() {}
F.prototype = o
return new F()
}
测试下
1
2
3
4
5
6
7
8
9
10
11
12
13
14var a = Object.create({name: 'cpp'})
console.log(a) // name属性放在了首层下的__proto__属性上,
/*
{}
__proto__:
name: 'cpp'
*/
接着用 createObj测试
var b = createObj({name: 'cpp'})
/*
{}
__proto__:
name: 'cpp'
*/
寄生组合式继承
1 | function Parent (name) { |
组合继承最大的缺点就是会两次调用父构造函数,
一次是子类型实例的原型的时候
1 | Child.prototype = new Parent(); |
另外一次是创建子类型的实例的时候
1 | var child1 = new Child('kevin', '18'); |
如何避免一次重复调用呢
1 | Child.prototype = Object.create(Parent.prototype); |
最后实现寄生组合继承如下
1 | function Parent (name) { |
proto是隐式原型,prototype是显式原型
原型总结
每个对象都有一个隐式原型,指向该对象的原型。实例化后通过proto属性指向构造函数的显式原型prototype,原型链是由原型对象组成,每个对象都有proto属性,指向创建该对象的构造函数的原型,通过隐式原型proto属性将对象链起来,组成原型链,用来实现属性继承和共享属性