this、call、apply、bind的理解及使用
1、this指向的四种情况
1.1、"new" 出实例
function Person(name) { this.name = name console.log(this) } const person = new Person('test-text') // this指向当前person实例对象 复制代码
1.2、默认指向window
function fn() { console.log(this) } fn() // 浏览器window,node里global 复制代码
1.3、对象调用方法
const target = { fn: function () { console.log(this) } } target.fn() // target const fn = target.fn fn() // 浏览器window,node里global let obj = { a: function () { console.log(this) }, } obj.a() //打出的是obj对象 const obj = { // 箭头函数中 a: () => { console.log(this) }, b: this } console.log('obj', obj) // {b: Window, a: ƒ} obj.a() //打出来的是window 复制代码
1.4、call、apply、bind改变this
const obj1 = { name: 'test', sayName: function() { console.log(this.name) } } const obj2 = { name: 'test-text' } // 改变sayName的this指向obj2 obj1.sayName.call(obj2) // test-text // 改变sayName的this指向obj2 obj1.sayName.apply(obj2) // test-text // 改变sayName的this指向obj2 const fn = obj1.sayName.bind(obj2) // 还需手动调用 fn() // test-text 复制代码
总结:
1、普通函数中,this指向最终调用者
2、箭头函数中,默认绑定自己当前作用域链的上一层的this
2、call/apply/bind的基本介绍
2.1、语法:
fun.call(thisArg, param1, param2, ...) fun.apply(thisArg, [param1,param2,...]) fun.bind(thisArg, param1, param2, ...) 复制代码
2.2、返回值:
call / apply
:fun
执行的结果;
bind
:返回fun
的拷贝(还需手动调用),并拥有指定的this
值和初始参数
2.3、区别:
2.3.1、call与apply的唯一区别,传给fun
的参数写法不同:
call
从第2~n的参数都是传给fun
的;apply
是第2个参数,这个参数是一个数组:传给fun
参数都写在数组中。
2.3.2、call/apply与bind的区别
call / apply
改变了函数的this
上下文后马上执行该函数bind
则是返回改变了上下文后的函数,不执行该函数
2.4、核心理念:借用方法
程序中:
A对象有个方法,B对象因为某种原因也需要用到同样的方法,那么这时候我们是单独为 B 对象扩展一个方法呢,还是借用一下 A 对象的方法呢?
当然是借用 A 对象的方法啦,既达到了目的,又节省了内存。
这就是call/apply/bind的核心理念:借用方法。
借助已实现的方法,改变方法中数据的this指向,减少重复代码,节省内存。
3、call和apply的应用场景
这些应用场景,多加体会就可以发现它们的理念都是:借用方法
3.1、判断数据类型:
四种常见的判断数据类型的方法,可以看一下我的另外一篇文章:
JS中8种数据类型、4种类型检测方法总结
Object.prototype.toString
用来判断类型再合适不过,借用它我们几乎可以判断所有类型的数据:
function isType(data, type) { const typeObj = { '[object String]': 'string', '[object Number]': 'number', '[object Boolean]': 'boolean', '[object Null]': 'null', '[object Undefined]': 'undefined', '[object Object]': 'object', '[object Array]': 'array', '[object Function]': 'function', '[object Date]': 'date', // Object.prototype.toString.call(new Date()) '[object RegExp]': 'regExp', '[object Map]': 'map', '[object Set]': 'set', '[object HTMLDivElement]': 'dom', // document.querySelector('#app') '[object WeakMap]': 'weakMap', '[object Window]': 'window', // Object.prototype.toString.call(window) '[object Error]': 'error', // new Error('1') '[object Arguments]': 'arguments', } let name = Object.prototype.toString.call(data) // 借用Object.prototype.toString()获取数据类型 let typeName = typeObj[name] || '未知类型' // 匹配数据类型 return typeName === type // 判断该数据类型是否为传入的类型 } console.log( isType({}, 'object'), // true isType([], 'array'), // true isType(new Date(), 'object'), // false isType(new Date(), 'date'), // true ) 复制代码
3.2、类数组借用数组的方法:
类数组因为不是真正的数组所有没有数组类型上自带的种种方法,所以我们需要去借用数组的方法。
比如借用数组的push
方法:
var arrayLike = { 0: 'OB', 1: 'Koro1', length: 2 } Array.prototype.push.call(arrayLike, '添加元素1', '添加元素2'); console.log(arrayLike) // {"0":"OB","1":"Koro1","2":"添加元素1","3":"添加元素2","length":4} 复制代码
3.3、apply获取数组最大值最小值:
apply
直接传递数组做要调用方法的参数,也省一步展开数组,比如使用Math.max
、Math.min
来获取数组的最大值/最小值:
const arr = [15, 6, 12, 13, 16]; const max = Math.max.apply(Math, arr); // 16 const min = Math.min.apply(Math, arr); // 6 复制代码
3.4、继承:
ES5
的继承也都是通过借用父类的构造方法来实现父类方法/属性的继承:
// 父类 function supFather(name) { this.name = name; this.colors = ['red', 'blue', 'green']; // 复杂类型 } supFather.prototype.sayName = function (age) { console.log(this.name, 'age'); }; // 子类 function sub(name, age) { // 借用父类的方法:修改它的this指向,赋值父类的构造函数里面方法、属性到子类上 supFather.call(this, name); this.age = age; } // 重写子类的prototype,修正constructor指向 function inheritPrototype(sonFn, fatherFn) { sonFn.prototype = Object.create(fatherFn.prototype); // 继承父类的属性以及方法 sonFn.prototype.constructor = sonFn; // 修正constructor指向到继承的那个函数上 } inheritPrototype(sub, supFather); sub.prototype.sayAge = function () { console.log(this.age, 'foo'); }; // 实例化子类,可以在实例上找到属性、方法 const instance1 = new sub("OBKoro1", 24); const instance2 = new sub("小明", 18); instance1.colors.push('black') console.log(instance1) // {"name":"OBKoro1","colors":["red","blue","green","black"],"age":24} console.log(instance2) // {"name":"小明","colors":["red","blue","green"],"age":18} 复制代码
类似的应用场景还有很多,就不赘述了,关键在于它们借用方法的理念,不理解的话多看几遍。
3.5、call、apply,该用哪个?
call,apply
的效果完全一样,它们的区别也在于
参数数量/顺序确定就用call,参数数量/顺序不确定的话就用apply。
考虑可读性:参数数量不多就用
call
,参数数量比较多的话,把参数整合成数组,使用apply
。参数集合已经是一个数组的情况,用
apply
,比如上文的获取数组最大值/最小值。
参数数量/顺序不确定的话就用apply
,比如以下示例:
const obj = { age: 24, name: 'OBKoro1', } const obj2 = { age: 777 } callObj(obj, handle) callObj(obj2, handle) // 根据某些条件来决定要传递参数的数量、以及顺序 function callObj(thisAge, fn) { let params = [] if (thisAge.name) { params.push(thisAge.name) } fn.apply(thisAge, params) // 数量和顺序不确定 不能使用call } function handle(...params) { console.log('params', params) // do some thing } 复制代码
4、bind的应用场景
4.1. 保存函数参数:
首先来看下一道经典的面试题:
for (var i = 1; i <= 5; i++) { setTimeout(function test() { console.log(i) // 依次输出:6 6 6 6 6 }, i * 1000); } 复制代码
造成这个现象的原因是等到setTimeout
异步执行时,i
已经变成6了。
关于js
事件循环机制不理解的,可以看这篇博客:
Js 的事件循环(Event Loop)机制以及实例讲解
那么如何使他输出: 1,2,3,4,5呢?
方法有很多:
4.1.1、闭包, 保存变量
for (var i = 1; i <= 5; i++) { (function (i) { setTimeout(function () { console.log('闭包:', i); // 依次输出:1 2 3 4 5 }, i * 1000); }(i)); } 复制代码
在这里创建了一个闭包(就是能够读取其他函数内部变量的函数),每次循环都会把i
的最新值传进去,然后被闭包保存起来。
4.1.2、bind
for (var i = 1; i <= 5; i++) { // 缓存参数 setTimeout(function (i) { console.log('bind', i) // 依次输出:1 2 3 4 5 }.bind(null, i), i * 1000); } 复制代码
实际上这里也用了闭包,我们知道bind会返回一个函数,这个函数也是闭包。
它保存了函数的this
指向、初始参数,每次i
的变更都会被bind
的闭包存起来,所以输出1-5
4.1.3、let
用let
声明i
也可以输出1-5,因为let
是块级作用域,所以每次都会创建一个新的变量,所以setTimeout
每次读的值都是不同的。
作者:沃斯桢蒂帥
链接:https://juejin.cn/post/7038768983146758180