call、apply、bind、new的实现

102 阅读9分钟

按值传递

js有两种数据类型,分别为基本数据类型和引用数据类型,两种数据类型的存储空间不同。

概念:将函数外部的值通过参数的方式赋值给函数内部的参数

两者传参区别

var value = 1;
function foo(v) {
//在这一步之前做了一步:var v = value
    v = 2;
    console.log(v); //2
}
//此时传递的参数就是value的副本,并不是value本身
foo(value);
console.log(value) // 1

在基本类型中,数据是存储在栈内存中的,因为基础数据类型的大小是固定的,传递参数的时候,相当于拷贝了一份当前值的副本,函数中修改的都是副本的值,永远不会改变原来的值。

var obj = {
    value: 1
};
function foo(o) {
    o.value = 2;
    console.log(o.value); //2
}
foo(obj);
console.log(obj.value) //2

上述传参,obj对象存放在栈内存中的指针复制一份存放在栈内存中,两个指针的指向是一样的,都是指向堆内存中obj对象实际的值,由于两个指针指向的是堆内存中同一个对象,所以一旦将对象身上的属性值进行修改,则会影响原始数据。若将栈内存的指针进行改变,则两个对象的指针不同,指向堆内存中的对象不同,所以并不会改变原来变量的值。

思考:下图为什么不会互相影响?

var obj = {
    value: 1
};
function foo(o) {
     o = {
       value: 2  //此时两个对象并不会互相影响
     };
}
foo(obj);
console.log(obj)

面试话语总结:函数传参本质上都是按值传递。基本数据类型传参是传递实际的值,不会互相影响。引用类型参数传递,传递是指针地址,若未修改指针地址,则指向的则是堆内存中同一个对象,修改属性互相影响。若修改了指针地址,则两个指针指向的是堆内存中两个不同的对象,所以互不干涉。因为复制指针也是在栈中操作的,跟基本数据类型复制差不多,也是一种按值传递,所以在函数中传参只有一种:按值传递。

为什么引用类型实际的值存储在堆内存中:在引用类型中,我们可以不停的给对象身上添加方法和属性,大小是无法确定的

手写call、apply、bind

三者作用:根据传入的this去改变当前this的指向

call

    let foo = {
      value = 1
    }
    function bar(){
      console.log(this.value)
    }
    bar() //此时输出undefined
    bar.call(foo) // 1
    相当于:
    let foo = {
      value:1,
      bar:function(){
       console.log(this.value)
      }
    }
实现思路:
  1. 将传入的函数设为对象身上的属性
  2. 执行该函数
  3. 删除改函数
`简易版本:`
Function.prototype.call2 = function(context) { 
//实现上述步骤一:按照bar.call(foo)理解,此时this的指向是bar函数
//此时的this指向的是bar,将bar函数设置为对象身上的属性
context.fn = this;  
//此步骤是改变了this的指向,将this指向了context
context.fn(); 
delete context.fn;
}

//测试代码:
let foo = {
    value: 1
};
function bar() {
    console.log(this.value);
}
bar.call2(foo); // 1

`上述方法中this的指向是传入的foo,相当于foo.bar()`
let foo = {
    value: 1,
    bar: function() {
        console.log(this.value)
    }
};
foo.bar(); // 1

`可传参版本:`

Function.prototype.call2 = function(context,...args) { 
//使用arguments去接收参数
context.fn = this;
context.fn(...args); 
delete context.fn;
}

//测试代码:
var foo = { value: 1 };
function bar(name, age) { 
console.log(name) 
console.log(age) 
console.log(this.value);
} 
bar.call2(foo, 'kevin', 18);

`兼容this为空版本:`

Function.prototype.call2 = function(context,...args) { 
//使用arguments去接收参数
var context = context || window
context.fn = this;
context.fn(...args); 
delete context.fn;
}
bar.call2(null, 'kevin', 18);

`兼容直接使用return的版本:`

Function.prototype.call2 = function(context,...args) { 
  //使用arguments去接收参数
  var context = context || window
  context.fn = this;
  let result =  context.fn(...args); 
  delete context.fn;
  return result
}

//测试代码:
var obj = { value: 1 };
function bar(name, age) {
   console.log(this.value);
   return { value: this.value, name: name, age: age };
}
console.log(bar.call2(obj, 'kevin', 18));
`完美版本:`
Function.prototype.call2 = function(context, ...args) {
  // 判断是否是undefined和null
  if (typeof context === 'undefined' || context === null) {
    context = window
  }
  let fnSymbol = Symbol() //这一步是保证了函数的唯一性
  context[fnSymbol] = this
  let fn = context[fnSymbol](...args)
  delete context[fnSymbol] 
  return fn
}
//测试代码:

 var obj = { value: 1, fn: 3 };
      function bar(name, age) {
        console.log(this.value);
        console.log(this.fn);
      }
 console.log(bar.call2(obj, 'kevin', 18))
 
 //若未将函数唯一性则打印出来的是:
 1和ƒ bar(name, age) {
        console.log(this.value);
        console.log(this.fn);
      }

apply

和上述call实现的方式相同,接受参数不同,call是将参数以字符串的形式依次传入,apply则是将所有参数放在数组中传入

Function.prototype.apply2 = function(context, args) {
  // 判断是否是undefined和null
  if (typeof context === 'undefined' || context === null) {
    context = window
  }
  let fnSymbol = Symbol() //这一步是保证了函数的唯一性
  context[fnSymbol] = this
  let fn = context[fnSymbol](args)
  delete context[fnSymbol] 
  return fn
}

bind

作用:

bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

实现思路:
  1. 返回一个函数
  2. 将传入的参数进行合并
Function.prototype.bind2 = function (context,...args) {
        let self = this //此时将this的指向先缓存起来
        return function(...bindArgs){
          //改变this的指向
          return self.apply(context,[...args,...bindArgs])
        }
};
//测试代码
var foo = {
        value: 1
      };
   function bar(name,age) {
        console.log(this.value); //1
        console.log(name);//lili
        console.log(age); //12
   }
   // 返回了一个函数
  var bindFoo = bar.bind2(foo,'lili');
  bindFoo(12); //

手写new

实现原理:

创造一个对象类型的实例或者构造函数的内置对象

如何实现

  1. 返回一个新的对象
  2. 新对象需要继承传入构造函数身上的所有属性和方法
  3. 修改this的指向,指向新创建的对象
`基础版本:`
function objectFactory(context,...args){
   const obj = new Object()
   //继承传入的对象身上的所有属性和方法
   obj.__proto__ = context.prototype
   //修改this的指向问题
   context.apply(obj,[...args])
   return obj
}

//测试代码:
function Person (name, age) {
    this.name = name;
    this.age = age;
    this.habit = 'Games';
    this.strength = 60;
}
var person = new Person('Kevin', '18');
objectFactory(Person,'Kevin', '18')
//上述两者等价相等
console.log(person.strength) // 60
console.log(person.age) // 18

`进阶版:解决传入的对象存在return的情况:`
function objectFactory(context,...args){
   const obj = new Object();
   obj.__proto__ = context.prototype;
   const result = context.apply(obj, [...args]);
   return typeof result === 'object' ? result : obj;
}
//测试代码
     function Person(name, age) {
        this.name = name;
        this.age = age;
        this.habit = 'Games';
        return {
          name,
          age
        };
      }

      Person.prototype.strength = 60;

      Person.prototype.sayYourName = function () {
        console.log('I am ' + this.name);
      };
      var person = objectFactory(Person, 'Kevin', '18');

      console.log(person.name); // Kevin
      console.log(person.habit); // undefined
      person.sayYourName(); // undefined
`上述代码坑点:若构造函数一旦返回了一个对象,那么就会打断继承,直接从返回的对象身上取属性,而不是从构造函数身上取属性和方法`

类数组对象和arguments

概念:

拥有一个 length属性和若干索引属性的对象,和数组的读写方法一致,但调用数组的原生方法会报错

//数组
var array = ['name', 'age', 'sex'];
//类数组
var arrayLike = {
    0: 'name',
    1: 'age',
    2: 'sex',
    length: 3
}

调用方式

var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 } Array.prototype.join.call(arrayLike, '&'); // name&age&sex Array.prototype.slice.call(arrayLike, 0); // ["name", "age", "sex"] // slice可以做到类数组转数组 Array.prototype.map.call(arrayLike,function(item){ 
  return item.toUpperCase(); 
});

只能通过Function.call 间接调用

转化成数组

var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }
// 1. slice
Array.prototype.slice.call(arrayLike); // ["name", "age", "sex"] 
// 2. splice
Array.prototype.splice.call(arrayLike, 0); // ["name", "age", "sex"] 
// 3. ES6 Array.from
Array.from(arrayLike); // ["name", "age", "sex"] 
// 4. apply
Array.prototype.concat.apply([], arrayLike)

arguments

概念:

在函数体中,可以访问到函数体里面的所有参数和其他的属性

继承的多种方式

概念:

一个对象身上的属性和方法较少,想使用其他对象身上的属性和方法

方式:

  1. 原型链继承
function Person(name) {}
Person.prototype.name = 'xianzao';
Person.prototype.getName = function () {
   console.log(this.name);
};
function Child() {}
const child = new Child();
Child.prototype = new Person();
// const child = new Child();
console.log('child', child.name);
`思考:为什么先创建child实例后修改原型之后继承失败,无法访问到person对象身上的属性`

答案:先实例化后修改原型:在进行child实例化的时候,会产生一个隐式原型(__proto__属性)指向child的原型对象,此时child的原型对象身上是没有name属性和getName方法的。之后在对整个原型对象进行修改,相当于将它重新指向了堆内存中一个新的地址,然后我们去访问child.name是去隐式原型指向的那个原型对象身上找,所以是undefined
先修改原型后实例化:实例的原型指向child.prototype,而child.prototype已经指向Person.prototype了,所以可以访问到peroson对象身上的name属性

特征:原型身上的属性被所有实例所共享,一旦修改某个实例身上的引用类型属性,会影响其他实例

function Parent () {
    this.names = ['xianzao', 'zaoxian'];
    this.age = 12
}
function Child () {
}
Child.prototype = new Parent();
var child1 = new Child();
child1.names.push('test');
console.log(child1.names); // ["xianzao", "zaoxian", "test"]
var child2 = new Child();
console.log(child2.names); // ["xianzao", "zaoxian", "test"]
  1. 借用构造函数
function Parent () {
    this.names = ['xianzao', 'zaoxian'];
}
function Child () {
    Parent.call(this);//传入this是显式的将当前函数上下文传入
}
var child1 = new Child();
child1.names.push('test');
console.log(child1.names); // ["xianzao", "zaoxian", "test"]
var child2 = new Child();
console.log(child2.names); // ["xianzao", "zaoxian"]

特征:避免了引用类型的属性被所有实例共享

思考:在构造函数中定义的属性和方法和定义在原型上面的属性和方法有什么区别

1、定义在构造函数中的属性存在于实例本身,只能通过实例对象访问,不能通过原型链去访问

2、会被重复创建,实例化一次就会被创建一次

3、由第一条可知,此时子类是没有办法继承这个属性的

  1. 组合继承:属性走借用构造函数继承,方法走原型链继承
function Parent (name) { 
  this.name = name; 
  this.colors = ['red', 'blue', 'green']; 
} 
Parent.prototype.getName = function () {
   console.log(this.name) 
} 
function Child (name, age) {
  Parent.call(this, name);
  this.age = age; 
}

  1. 原型式继承:接收一个对象,增强当前对象的功能(相当于Object.create(obj))
function createObj(o) {
    function F(){}
    F.prototype = o;
    return new F();
}

缺点:属性进行了共享,和原型链继承一样

优点:使用了较为简单的方式去实现继承

  1. 寄生式继承:传入一个对象,对对象身上的属性和方法进行扩展,最终返回这个对象
function createObj (o) {
    var clone = Object.create(o);
    clone.sayName = function () {
        console.log('hi');
    }
    return clone;
}
const obj = createObj({a:1,b:1})

优点:简单,无需使用构造函数去实例化一个对象

创建对象的几种方式

  1. 工厂模式(显式的通过new一个新的对象去创建,最后将这个对象返回出去,调用的时候传递对应的参数)
function createObj(name) {
  const obj = new Object();
  obj.name = name;
  obj.getName = function () {
    console.log('getName', name);
  };
  return obj;
}
const obj1 = createObj('lili');
obj1.getName();

优点:方式简单

缺点:所有实例的原型都相同

  1. 构造函数模式
      function Person(name,age){
        this.name = name
        this.age = age
        this.getName=function(){
            console.log("构造函数",name)
        }
      }
      const person = new Person("lili",12)
      console.log(person.getName())

优点:实例身上的属性不会被其他实例影响

缺点:每次创建实例时,方法都要被创建一次

优化:可以先创建一个引用,后续调用的时候再去找

    function Person(name,age){
        this.name = name
        this.age = age
        this.getName = getName
      }
      
      function getName(){
        console.log(name)
      }
      const person = new Person("lili",12)
      console.log(person.getName())

3.原型模式

      function protoObj() {}
      protoObj.prototype.name = 'lili';
      protoObj.prototype.age = 12;
      protoObj.getName = function () {
        console.log('name', this.name);
      };
      const proto = new protoObj();
优点:方法只会创建一次
缺点:无法进行初始化,所有的属性和方法被实例所共享

如何解决?

4.组合模式(构造函数与原型相结合,该共享的共享,该私有就私有)

function combine() {
        this.name = name;
      }
      combine.prototype = {
        getName: function () {
          console.log(this.name);
      }
 };
 const obj = new combine("lili")
 obj.getName()