重学JavaScript 篇的目的是回顾基础,方便学习框架和源码的时候可以快速定位知识点,查漏补缺,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。
只有更好的了解函数的内部结构,才可以更好的封装方法。
arguments
arguments 表示一个函数传入的参数,不管有没有传参,arguments 都存在。
function fn(){
console.log(arguments);
}
fn(1, 2, 3);
// Arguments [1, 2, 3, callee: fn(), Symbol(Symbol.iterator)]
fn();
// undefined
可以看出有参数传入的话,arguments就是一个类数组,保存了传入的值,如果没有参数,就是undefined。既然arguments可以获得参数,那当然可以使用arguments操作参数了,如果赋值了arguments再传参呢?
function fn(){
arguments[1] = 5;
console.log(arguments);
}
fn(1);
// Arguments [1, 5, callee: fn(), Symbol(Symbol.iterator)]
注意:如果只传了一个参数,然后把 arguments[1] 设置为某个值,那么这个值不会反映到第二个命名参数,因为arguments的长度是根据传入的参数个数,而不是定义函数时给出的命名参数个数。
如果使用箭头函数,是无法获取 arguments 的。
arguments 有一个 callee 属性,它指向 arguments 所在的函数,下面是一个通过递归阶乘的函数:
function factorial(num){
if(num <= 1){
return 1;
} else {
return num * factorial(num - 1);
}
}
内部调用了 factorial 自己,相当于上面的代码写了两次 factorial,通过 arguments.callee 就可以只写一次,且易读:
function factorial(num){
if(num <= 1){
return 1;
} else {
return num * arguments.callee(num - 1);
}
}
好处是什么呢?现在还体现不出来,再看个例子:
let factorial1 = factorial;
factorial = function(){
return 0;
};
console.log(factorial1(5)); // 120
console.log(factorial(5)); // 0
相当于原来 factorial 的指针又指向了一个新地址:factorial1,然后重写了 factorial。这样如果不使用 arguments.callee 的话,factorial 内部调用的其实不是自己,而是 factorial,使用了 arguments.callee 之后,调用的就是自己了。
callee 英文是被叫者的意思,也可以理解为被叫者就是所在的函数。
caller
caller 是呼叫者的意思,指向函数被调用者:
function fn(){
gn();
}
funciton gn(){
console.log(gn.caller);
}
fn();
上面的代码会打印fn函数本身,因为 fn 调用了 gn,所以 gn 的 caller 就指向了 fn,而 gn 其实也是arguments.callee,所以可以这样写:
funciton gn(){
console.log(arguments.callee.caller);
}
new.target
ECMAScript6定义了 new.target 属性,用于检测函数是否是用 new关键字 调用的,如果不是就返回undefined,如果是就引用被调用的构造函数:
function fn(){
if(!new.target){
console.log("没使用new")
} else {
console.log("使用了new")
}
}
new fn(); // 使用了new
fn(); // 没使用new
call
call 可以改变函数体内 this 的指向:
function sum(a, b){
return a + b;
}
function fn(a, b){
return sum.callthis, a, b);
}
fn(1, 2); // 3
上面的例子中,fn 调用了 sum,将 fn 内部的 this 传入了 sum,也可以说 this 指向了 sum,同时传入了 arguments。
下面我们实现一个 call:
首先我们分析 call 干了什么事情,改变了this的指向,并且执行了原来的函数,假如是这样调用的:
var obj = {
name: "abc"
}
function getName(){
console.log(this.name);
}
getName.call(obj);
那么是不是可以理解为:getName是在obj上被调用了!
var obj = {
name: "abc",
getName: function(){
console.log(this.name)
}
}
obj.getName();
这样不就和上面的逻辑一样了么,但是这样就给obj无缘无故添加了一个方法,我们把这个方法删掉不就更逼真了么:
var obj = {
name: "abc",
getName: function(){
console.log(this.name)
}
}
obj.getName();
delete obj.getName;
到这里,我们从头分析一下走了哪几步:
- 将函数设置成对象的属性
- 执行这个函数
- delete 删除这个函数
那么:
Function.prototype.myCall = function(context){
// this就是call的函数
context.fn = this; // obj.fn = getName
context.fn(); // obj.fn()
delete context.fn; // delete obj.fn
}
一个简单版本的call就重写好了,接下来增加参数,原来的call是这样使用的。
var obj = {
name: "abc",
}
function getName(first, last){
console.log(first);
console.log(last);
console.log(this.name)
}
getName.call(obj, "a", "bc"); // "a", "bc", "abc"
传入的参数不确定!可以使用前面提到的 arguments!取出第二个到最后一个参数,然后放到一个数组里!
// 上面的那个例子里 arguments是
// arguments = {
// 0: obj,
// 1: "a",
// 2: "b",
// length: 3
// }
var args = [];
for(var i = 1; i < arguments.length; i++){
args.push('arguments[' + i + ']');
}
// [obj, "a", "b"]
然后我们要把args放到代码里:
Function.prototype.myCall = function(context){
// this就是call的函数
context.fn = this; // obj.fn = getName
var args = [];
for(var i = 1; i < arguments.length; i++){
args.push('arguments[' + i + ']');
}
eval('context.fn(' + args + ')');
delete context.fn; // delete obj.fn
}
到这里要注意两个问题,如果this没有呢?并且函数是可以有返回值的!直接上代码
Function.prototype.myCall = function(context){
// this 如果没有的话,默认会指向window,
// this可以传基本类型数据,这种情况的话原生的call处理方式
// 是将参数用Object()转换一下
var context = context ? Object(context) : window;
// this就是call的函数
context.fn = this; // obj.fn = getName
var args = [];
for(var i = 1; i < arguments.length; i++){
args.push('arguments[' + i + ']');
}
var result = eval('context.fn(' + args + ')');
delete context.fn; // delete obj.fn
return result;
}
apply
apply 也可以改变函数体内 this 的指向,和 call 用法一样,只不过第二个参数是数组或类数组:
function sum(a, b){
return a + b;
}
function fn(){
return sum.apply(this, arguments);
}
fn(1, 2); // 3
下面我们实现一个apply:
Function.prototype.myApply = function (context, arr) {
var context = Object(context) || window;
context.fn = this;
var result;
if (!arr) {
result = context.fn();
}
else {
var args = [];
for (var i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
result = eval('context.fn(' + args + ')')
}
delete context.fn
return result;
}
和call的区别就是一个是用arguments,一个是用传入的arr数组。
bind
bind 会创建一个新的函数,在bind被调用的时候,这个新函数的 this 就被指定为 bind 的第一个参数,其余参数将作为新函数的参数传入,供调用时使用。
也就是说,bind方法会创建一个新的函数实例,this也会绑定到这个函数实例上:
var obj = {
name: "abc"
}
function fn(){
console.log(this.name);
}
var a = fn.bind(obj);
a(); // abc
上面例子将 fn 的 this 指向了 obj。
我们利用apply,来模拟一下:
Function.prorotype.myBind = function(context){
var _this = this;
return function(){
_this.apply(context);
}
}
接着优化,优化参数的传递:
Function.prorotype.myBind = function(context){
var _this = this;
// 获取第二个到最后一个参数
var args = Array.prototype.slice.call(arguments, 1);
return function(){
//此时的arguments是bind返回的函数的参数
var bindArgs = Array.prototype.slice.call(arguments);
_this.apply(context, args.concat(bindArgs));
}
}
到这里,算是一半了,注意:
一个绑定函数也能使用new操作符创建对象,这种行为就像把原函数当成了构造器。
提供的this值被忽略,同时调用的参数被提供给模拟参数。
也就是说 bind 返回的函数作为构造函数的时候,bind 指定的 this 会失效,但是传入的参数仍然生效。
Function.prorotype.myBind = function(context){
var _this = this;
// 获取第二个到最后一个参数
var args = Array.prototype.slice.call(arguments, 1);
var fnBound = funciton(){
var bindArgs = Array.prototype.slice.call(arguments);
// 当作为构造函数时,this指向实例
// 当作为普通函数时,this指向绑定的函数context
// isCreateOrCustom 是true的话,this指向实例
var isCreateOrCustom = this instanceof _this ? this : context;
_this.apply(isCreateOrCustom, args.concat(bindArgs));
}
// 修改函数的 prototype 为绑定函数的 prototype
// 实例就可以继承函数的原型中的值
fnBound.prototype = this.prototype;
return fnBound
}
如果此时直接修改 fBound.prototype 了,this.prototype也会改变,所以可以用一个空函数:
Function.prorotype.myBind = function(context){
var _this = this;
// 获取第二个到最后一个参数
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function(){}
var fnBound = funciton(){
var bindArgs = Array.prototype.slice.call(arguments);
// 当作为构造函数时,this指向实例
// 当作为普通函数时,this指向绑定的函数context
// isCreateOrCustom 是true的话,this指向实例
var isCreateOrCustom = this instanceof _this ? this : context;
_this.apply(isCreateOrCustom, args.concat(bindArgs));
}
// 修改函数的 prototype 为绑定函数的 prototype
// 实例就可以继承函数的原型中的值
fNOP.prototype = this.prototype;
fnBound.prototype = new fNOP();
return fnBound;
}
上面的两句:
fNOP.prototype = this.prototype;
fnBound.prototype = new fNOP();
可以修改为:
fbound.prototype = Object.create(this.prototype);
因为Object.create内部相当于:
Object.create = function (o) {
function f(){}
f.prototype = 0;
return new f;
}
我的公众号:道道里的前端栈,每一天一篇前端文章,嚼碎的感觉真奇妙~