js中的call
2021-02-08 本文已影响0人
攻城熊
java中对象包含属性和方法, 方法即函数, 只有对象可以调用方法
但是js中一切皆对象, 函数是对象, 函数也可以调用方法
Function.prototype.call
Function.prototype.apply
Function.prototype.bind
Function.prototype.toString
为什么js语言会在function原型上挂上这四个函数?
因为好处太多了, 现在我就说一说call的应用
// A对象
let a = {
name: "aaa",
toName(age) {
console.log(age, "我的名字:", this.name)
}
}
// B对象
let b = {
name: "bbb",
}
// A对象调用自己的方法
a.toName('A对象')
// A对象具有某种能力, 能打印自己的名字,但是如何与别人共享这种能力呢?
// 编程思想促进了语法的形成 ==> 于是js语法就设计了这种通用能力
a.toName.call(b, 'B对象')
再深入思考一下, call语法是基于什么形成的呢 ?
了解底层语法有助于我们理解高级的语法
因为在底层的语法眼中, 高级语法只不过是工具, 根本不算语法, 不过是由我定义构建处理来的
理解call函数, 核心必要知识
- 接受函数也可以调用函数这个观念(违反直觉, 需要编程思维, 原型 原型链 prototype)
- 词法作用域 作用域链(不同浏览器内核不同, 谷歌浏览器是v8设计出来的js执行机制, 底层整个系统基于算法 + 数据结构, 总之, 这就是js执行规则的制定者)
- 谁是this, 即谁在调用函数
函数调用, 需要知道函数当前的上下文信息,
因为函数内部的作用域可能会引用函数拥有者的相关信息, 函数拥有者就是函数的上下文,
找不到需要的信息, 通过作用域链就会在全局作用域寻找
Object.prototype.mycall = function(obj){
// a.toName.mycall(b, 'B对象')
// 拆开分析 , 上面是我们的目标
// 1. 可以看出, a.toName就是我们要写的这个函数的调用者, 'B对象',就是传入的第二个参数
let fn = this
// 此时fn就是我们要借用的函数 a.toName 用来打印名字, 但是此时它打印的名字依然是A对象的
// 2. 如何让它打印传入对象obj, 也就是b的名字呢?
// 作用域分析, 方法一: 将A对象与B对象交换作用域 (v8也许有,但我不知道)
// 方法二: 打不过就加入, A对象抄袭B对象, 将函数复制一份, 变成自己的函数, 然后调用自己的函数
obj.copyFn = fn
obj.copyFn()
}
call的本质并不是调用别人的函数, 而是copy了别人的函数
哈哈哈, v8内部的不知道, 反正js基于现有语法就有这个能力 (还有解法, 基于proto,原型链的方式, var temp = Object.create(null)), 利用干净的地方, 防止覆盖原对象的函数
Function.prototype.call = function(ctx, ...args) {
var proto = null
if ('__proto__' in ctx) {
proto = ctx.__proto__
}
var temp = Object.create(proto)
temp.$f = this
ctx.__proto__ = temp
var result = ctx.$f(...args)
ctx.__proto__ = proto
return result
}
显然, 我们自己的上面的mycall还要很多问题
- 将传入的对象添加了copyFn函数, 可能重置掉对象原有的copyFn, 修改了对象, 需要delete删除
- 传入对象需要进行类型判断
- 传入参数需要处理
- 返回值
Object.prototype.mycall = function (obj) {
obj = obj || window //node环境下无法使用window
// 这里如果害怕属性覆盖可以设置唯一值, 这里就不展开了
// Object.getOwnPropertyNames() // 获得对象的自己的属性[]
let fn = this
//给context添加一个方法 指向this
obj[fn] = this
// 处理参数 去除第一个参数this 其它传入fn函数
let arg = [...arguments].slice(1)
// 或者使用rest运算符获取剩余参数
// function f(a, ...arg) {
// console.log(arg, Array.isArray(arg))
// }
// f() //[] true
// f(1) //[] true
// f(1, 2) //[2] true
// f(1, 2, 3) //[2, 3] true
let res = context[fn](...arg) //执行fn
delete context[fn] //删除属性
return res
}
这里我们就实现了自己的call, 以后看见call就知道都不过是些雕虫小技了(当然真实的实现方式肯定需要考虑更多因素和性能, 而且在v8更大的环境下, 可能还不止一种实现方式,保持谦虚)
call的雕虫小技
-
Array, String等对象函数的借调
Array.prototype.slice.call(arguments)
[].slice.call(arguments)
Object.prototype.toString.call(arr).slice(8, -1) // Array -
实现借用构造函数继承
function Person(name,age,love){
this.name=name;
this.age=age;
this.love=love;
this.say=function say(){
alert("姓名:"+name);
}
}
//call方式
function student(name,age){
Person.call(this,name,age);
}
- vue源码中也有相关应用先写这么多了哈.