JS高级用法02学习笔记

257 阅读27分钟

JS高级用法02

课程目标

  • 参数按值传递
  • 手写call和apply
  • 手写bind
  • 手写模拟new
  • 类数组对象与arguments

知识要点

参数按值传递

在《JavaScript高级程序设计》中提到传递参数:

ECMAScript中所有函数的参数都是按值传递的。

什么是按值传递呢?

把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。

按值传递

例如:

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

当传递value到函数foo中,相当于拷贝了一份value,假设拷贝的这份叫 _value,函数中修改的都是 _value 的值,而不会影响原来的 value 值。

共享传递

引用传递,就是传递对象的引用,函数内部对参数的任何改变都会影响该对象的值,因为两者引用的是同一个对象。

例:

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

共享传递是指,在传递对象的时候,传递的是地址索引。

所以修改 o.value,可以通过引用找到原值,但是直接修改 o,并不会修改原值。所以第二个和第三个例子其实都是按共享传递。

参数如果是基本类型是按值传递,如果是引用类型按共享传递。

换句话说,函数传递参数 ,传递的是参数的拷贝:

  1. 指针拷贝,拷贝的是地址索引;
  2. 常规类型拷贝,拷贝的是值 ;

所以,一共是两种传递方式,按值传递和按共享传递。

总结

javascript中数据类型分为基本类型与引用类型:

  1. 基本类型值存储于栈内存中,传递的就是当前值,修改不会影响原有变量的值;
  2. 引用类型值其实也存于栈内存中,只是它的值是指向堆内存当中实际值的一个地址;索引引用传递传的值是栈内存当中的引用地址,当改变时,改变了堆内存当中的实际值;

所以针对上述的内容:

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

内存分布:

改变前:

栈内存堆内存
value1
v1

改变后:

栈内存堆内存
value1
v2

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

改变前:

栈内存堆内存
obj 指针地址{value:1}
o 指针地址{value:1}

改变后:

栈内存堆内存
obj 指针地址{value:2}
o 指针地址{value:2}

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

改变前:

栈内存堆内存
obj 指针地址{value:1}
o 指针地址{value:1}

改变后:

栈内存堆内存
obj 指针地址{value:1}
o 指针地址

手写call和apply

手写call

call() :在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。

let foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call(foo); // 1

注意两点:

  1. call 改变了 this 的指向,指向到 foo
  2. bar 函数执行了;
  • 第一步

    1. 将函数设为对象的属性;

    2. 执行该函数;

    3. 删除该函数;

      //第一版
      Function.prototype.call2 = function(context){
          //首先要获取调用call的函数,用this可以获取
          context.fn = this;
          context.fn();
          delete context.fn;
      }
      
  • 第二步

    call除了可以指定this,还可以指定参数

    //第二版
    Function.prototype.call2 = function(context){
        context.fn = this;
        let arg = [...arguments].slice(1);
        context.fn(...arg);
        delete context.fn;
    }
    
  • 第三步

    1. this 参数可以传 null,当为 null 的时候,视为指向 window

    2. 针对函数,可以实现返回值

      //最终版
      Function.prototype.call2 = function(context,...args){
          //判断context是否是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
      }
      

手写apply

apply 的实现跟 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,之后的一序列参数将会在传递的实参前传入作为它的参数。

bind 函数的两个特点:

  1. 返回一个函数;
  2. 可以传入参数;

返回函数的模拟实现

关于指定 this 的指向,我们可以使用 call 或者 apply 实现

// 第一版
Function.prototype.bind2 = function (context) {
    var self = this;

    // 考虑到绑定函数可能是有返回值的,加上return
    return function () {
        return self.apply(context);
    }

}

传参的模拟实现

当需要传 nameage 两个参数时,可以在 bind 的时候,只传一个 name,在执行返回的函数的时候,再传另一个参数 age

这里如果不适用rest,使用arguments进行处理:

//第二版
Function.prototype.bind2 = function(context){
    var self = this;
    //获取bind2函数从第二个参数开始的所有参数
    var args = Array.prototype.slice.call(arguments,1);
    return function(){
        //这个时候的arguments是指bind返回函数传入的参数
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(context,args.concat(bindArgs));
    }
}

构造函数效果的模拟实现

bind 还有一个特点,就是

一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

也就是说当 bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效。举个例子:

var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}

bar.prototype.friend = 'kevin';

var bindFoo = bar.bind(foo, 'daisy');

var obj = new bindFoo('18');
// undefined
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin

尽管在全局和 foo 中都声明了 value 值,最后依然返回了 undefind,说明绑定的 this 失效了

后文中new 的模拟实现,就会知道这个时候的 this 已经指向了 obj

//第三版
Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        // 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
        // 以上面的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改成 this ,实例会具有 habit 属性
        // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
        return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
    }
    // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
    fBound.prototype = this.prototype;
    return fBound;
}

构造函数效果的优化实现

但是在这个写法中,我们直接将 fBound.prototype = this.prototype,我们直接修改 fBound.prototype 的时候,也会直接修改绑定函数的 prototype。这个时候,我们可以通过一个空函数来进行中转:

// 第四版
Function.prototype.bind2 = function (context) {

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

最终版

Function.prototype.bind2 = function (context) {

    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}
//最终简化版
Function.prototype.myBind = function(context) {
// 判断是否是undefined 和 null
    if (typeof context === "undefined" || context === null) {
    	context = window;
    }
    self = this;
    return function(...args) {
    	return self.apply(context, args);
    }
}

手写模拟new

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例

new 进行的操作:

  1. 创建一个新的空对象
  2. 将空对象的_proto_指向构造函数的原型
  3. 改变this的指向,指向空对象
  4. 对构造函数的返回值做判断,返回对应的值

初步实现

//第一版
function objectFactory(){
    var obj  = new Object();
    Constructor = [].shift.call(arguments);
    obj._proto_ = Constructor.prototype;
    Constructor.apply(obj,arguments);
    return obj;
}

在这一版中,我们:

  1. 用new Object() 的方式新建了一个对象 obj;
  2. 取出第一个参数,就是我们要传入的构造函数。此外因为 shift 会修改原数组,所以 arguments 会被去除第一个参数;
  3. 将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性;
  4. 使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性;
  5. 返回 obj;

最终实现

添加返回值判断

//最终版
function objectFactory(){
    var obj = new Object();
    Constructor = [].shift.acll(arguments);
    obj._proto_ = Constructor.prototype;
    var ret = Constructor.apply(obj,arguments);
    return typeof ret === 'object' ? ret:obj;
}

类数组对象与arguments

类数组对象

拥有一个 length 属性和若干索引属性的对象,无法直接使用原生的数组方法

例:

var array = ['name','age','sex'];
var arrayLike = {
    0:'name',
    1:'age',
    2:'sex',
    length:3
}
读写
console.log(array[0]); // name
console.log(arrayLike[0]); // name

array[0] = 'new name';
arrayLike[0] = 'new name';
长度
console.log(array.length); // 3
console.log(arrayLike.length); // 3
遍历
for(var i = 0, len = array.length; i < len; i++) {
   ……
}
for(var i = 0, len = arrayLike.length; i < len; i++) {
    ……
}
调用数组方法

只能通过Function.call间接调用

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();
}); 
// ["NAME", "AGE", "SEX"]
类数组转数组的方法
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对象

Arguments 对象只定义在函数体中,包括了函数的参数和其他属性。在函数体中,arguments 指代该函数的 Arguments 对象。

length属性

Arguments对象的length属性,表示实参的长度。

例:

function foo(b, c, d){
    console.log("实参的长度为:" + arguments.length)
}

console.log("形参的长度为:" + foo.length)

foo(1)

// 形参的长度为:3
// 实参的长度为:1
callee属性

Arguments 对象的 callee 属性,通过它可以调用函数自身。

闭包经典面试题使用 callee 的解决方法:

var data = [];

for (var i = 0; i < 3; i++) {
    (data[i] = function () {
       console.log(arguments.callee.i) 
    }).i = i;
}

data[0]();
data[1]();
data[2]();

// 0
// 1
// 2
arguments 和对应参数的绑定
function foo(name, age, sex, hobbit) {

    console.log(name, arguments[0]); // name name

    // 改变形参
    name = 'new name';

    console.log(name, arguments[0]); // new name new name

    // 改变arguments
    arguments[1] = 'new age';

    console.log(age, arguments[1]); // new age new age

    // 测试未传入的是否会绑定
    console.log(sex); // undefined

    sex = 'new sex';

    console.log(sex, arguments[2]); // new sex undefined

    arguments[3] = 'new hobbit';

    console.log(hobbit, arguments[3]); // undefined new hobbit

}

foo('name', 'age')

传入的参数,实参和 arguments 的值会共享,当没有传入时,实参与 arguments 值不会共享

传递参数

将参数从一个函数传递到另一个函数

// 使用 apply 将 foo 的参数传递给 bar
function foo() {
    bar.apply(this, arguments);
}
function bar(a, b, c) {
   console.log(a, b, c);
}

foo(1, 2, 3)
转数组
function func(...arguments) {
    console.log(arguments); // [1, 2, 3]
}

func(1, 2, 3);

补充知识

JS高级用法02

课程目标

  • 参数按值传递
  • 手写call和apply
  • 手写bind
  • 手写模拟new
  • 类数组对象与arguments
  • 创建对象的多种方式&优缺点
  • 继承的多种方式&优缺点

知识要点

参数按值传递

在《JavaScript高级程序设计》中提到传递参数:

ECMAScript中所有函数的参数都是按值传递的。

什么是按值传递呢?

把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。

按值传递

例如:

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

当传递value到函数foo中,相当于拷贝了一份value,假设拷贝的这份叫 _value,函数中修改的都是 _value 的值,而不会影响原来的 value 值。

共享传递

引用传递,就是传递对象的引用,函数内部对参数的任何改变都会影响该对象的值,因为两者引用的是同一个对象。

例:

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

共享传递是指,在传递对象的时候,传递的是地址索引。

所以修改 o.value,可以通过引用找到原值,但是直接修改 o,并不会修改原值。所以第二个和第三个例子其实都是按共享传递。

参数如果是基本类型是按值传递,如果是引用类型按共享传递。

换句话说,函数传递参数 ,传递的是参数的拷贝:

  1. 指针拷贝,拷贝的是地址索引;
  2. 常规类型拷贝,拷贝的是值 ;

所以,一共是两种传递方式,按值传递和按共享传递。

总结

javascript中数据类型分为基本类型与引用类型:

  1. 基本类型值存储于栈内存中,传递的就是当前值,修改不会影响原有变量的值;
  2. 引用类型值其实也存于栈内存中,只是它的值是指向堆内存当中实际值的一个地址;索引引用传递传的值是栈内存当中的引用地址,当改变时,改变了堆内存当中的实际值;

所以针对上述的内容:

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

内存分布:

改变前:

栈内存堆内存
value1
v1

改变后:

栈内存堆内存
value1
v2

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

改变前:

栈内存堆内存
obj 指针地址{value:1}
o 指针地址{value:1}

改变后:

栈内存堆内存
obj 指针地址{value:2}
o 指针地址{value:2}

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

改变前:

栈内存堆内存
obj 指针地址{value:1}
o 指针地址{value:1}

改变后:

栈内存堆内存
obj 指针地址{value:1}
o 指针地址

手写call和apply

手写call

call() :在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。

let foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call(foo); // 1

注意两点:

  1. call 改变了 this 的指向,指向到 foo
  2. bar 函数执行了;
  • 第一步

    1. 将函数设为对象的属性;

    2. 执行该函数;

    3. 删除该函数;

      //第一版
      Function.prototype.call2 = function(context){
          //首先要获取调用call的函数,用this可以获取
          context.fn = this;
          context.fn();
          delete context.fn;
      }
      
  • 第二步

    call除了可以指定this,还可以指定参数

    //第二版
    Function.prototype.call2 = function(context){
        context.fn = this;
        let arg = [...arguments].slice(1);
        context.fn(...arg);
        delete context.fn;
    }
    
  • 第三步

    1. this 参数可以传 null,当为 null 的时候,视为指向 window

    2. 针对函数,可以实现返回值

      //最终版
      Function.prototype.call2 = function(context,...args){
          //判断context是否是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
      }
      

手写apply

apply 的实现跟 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,之后的一序列参数将会在传递的实参前传入作为它的参数。

bind 函数的两个特点:

  1. 返回一个函数;
  2. 可以传入参数;

返回函数的模拟实现

关于指定 this 的指向,我们可以使用 call 或者 apply 实现

// 第一版
Function.prototype.bind2 = function (context) {
    var self = this;

    // 考虑到绑定函数可能是有返回值的,加上return
    return function () {
        return self.apply(context);
    }

}

传参的模拟实现

当需要传 nameage 两个参数时,可以在 bind 的时候,只传一个 name,在执行返回的函数的时候,再传另一个参数 age

这里如果不适用rest,使用arguments进行处理:

//第二版
Function.prototype.bind2 = function(context){
    var self = this;
    //获取bind2函数从第二个参数开始的所有参数
    var args = Array.prototype.slice.call(arguments,1);
    return function(){
        //这个时候的arguments是指bind返回函数传入的参数
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(context,args.concat(bindArgs));
    }
}

构造函数效果的模拟实现

bind 还有一个特点,就是

一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

也就是说当 bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效。举个例子:

var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}

bar.prototype.friend = 'kevin';

var bindFoo = bar.bind(foo, 'daisy');

var obj = new bindFoo('18');
// undefined
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin

尽管在全局和 foo 中都声明了 value 值,最后依然返回了 undefind,说明绑定的 this 失效了

后文中new 的模拟实现,就会知道这个时候的 this 已经指向了 obj

//第三版
Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        // 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
        // 以上面的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改成 this ,实例会具有 habit 属性
        // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
        return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
    }
    // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
    fBound.prototype = this.prototype;
    return fBound;
}

构造函数效果的优化实现

但是在这个写法中,我们直接将 fBound.prototype = this.prototype,我们直接修改 fBound.prototype 的时候,也会直接修改绑定函数的 prototype。这个时候,我们可以通过一个空函数来进行中转:

// 第四版
Function.prototype.bind2 = function (context) {

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

最终版

Function.prototype.bind2 = function (context) {

    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}
//最终简化版
Function.prototype.myBind = function(context) {
// 判断是否是undefined 和 null
    if (typeof context === "undefined" || context === null) {
    	context = window;
    }
    self = this;
    return function(...args) {
    	return self.apply(context, args);
    }
}

手写模拟new

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例

new 进行的操作:

  1. 创建一个新的空对象
  2. 将空对象的_proto_指向构造函数的原型
  3. 改变this的指向,指向空对象
  4. 对构造函数的返回值做判断,返回对应的值

初步实现

//第一版
function objectFactory(){
    var obj  = new Object();
    Constructor = [].shift.call(arguments);
    obj._proto_ = Constructor.prototype;
    Constructor.apply(obj,arguments);
    return obj;
}

在这一版中,我们:

  1. 用new Object() 的方式新建了一个对象 obj;
  2. 取出第一个参数,就是我们要传入的构造函数。此外因为 shift 会修改原数组,所以 arguments 会被去除第一个参数;
  3. 将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性;
  4. 使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性;
  5. 返回 obj;

最终实现

添加返回值判断

//最终版
function objectFactory(){
    var obj = new Object();
    Constructor = [].shift.acll(arguments);
    obj._proto_ = Constructor.prototype;
    var ret = Constructor.apply(obj,arguments);
    return typeof ret === 'object' ? ret:obj;
}

类数组对象与arguments

类数组对象

拥有一个 length 属性和若干索引属性的对象,无法直接使用原生的数组方法

例:

var array = ['name','age','sex'];
var arrayLike = {
    0:'name',
    1:'age',
    2:'sex',
    length:3
}
读写
console.log(array[0]); // name
console.log(arrayLike[0]); // name

array[0] = 'new name';
arrayLike[0] = 'new name';
长度
console.log(array.length); // 3
console.log(arrayLike.length); // 3
遍历
for(var i = 0, len = array.length; i < len; i++) {
   ……
}
for(var i = 0, len = arrayLike.length; i < len; i++) {
    ……
}
调用数组方法

只能通过Function.call间接调用

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();
}); 
// ["NAME", "AGE", "SEX"]
类数组转数组的方法
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对象

Arguments 对象只定义在函数体中,包括了函数的参数和其他属性。在函数体中,arguments 指代该函数的 Arguments 对象。

length属性

Arguments对象的length属性,表示实参的长度。

例:

function foo(b, c, d){
    console.log("实参的长度为:" + arguments.length)
}

console.log("形参的长度为:" + foo.length)

foo(1)

// 形参的长度为:3
// 实参的长度为:1
callee属性

Arguments 对象的 callee 属性,通过它可以调用函数自身。

闭包经典面试题使用 callee 的解决方法:

var data = [];

for (var i = 0; i < 3; i++) {
    (data[i] = function () {
       console.log(arguments.callee.i) 
    }).i = i;
}

data[0]();
data[1]();
data[2]();

// 0
// 1
// 2
arguments 和对应参数的绑定
function foo(name, age, sex, hobbit) {

    console.log(name, arguments[0]); // name name

    // 改变形参
    name = 'new name';

    console.log(name, arguments[0]); // new name new name

    // 改变arguments
    arguments[1] = 'new age';

    console.log(age, arguments[1]); // new age new age

    // 测试未传入的是否会绑定
    console.log(sex); // undefined

    sex = 'new sex';

    console.log(sex, arguments[2]); // new sex undefined

    arguments[3] = 'new hobbit';

    console.log(hobbit, arguments[3]); // undefined new hobbit

}

foo('name', 'age')

传入的参数,实参和 arguments 的值会共享,当没有传入时,实参与 arguments 值不会共享

传递参数

将参数从一个函数传递到另一个函数

// 使用 apply 将 foo 的参数传递给 bar
function foo() {
    bar.apply(this, arguments);
}
function bar(a, b, c) {
   console.log(a, b, c);
}

foo(1, 2, 3)
转数组

使用ES6的 ... 运算符,可以轻松转成数组。

function func(...arguments) {
    console.log(arguments); // [1, 2, 3]
}

func(1, 2, 3);

补充知识

JS高级用法02

课程目标

  • 参数按值传递
  • 手写call和apply
  • 手写bind
  • 手写模拟new
  • 类数组对象与arguments

知识要点

参数按值传递

在《JavaScript高级程序设计》中提到传递参数:

ECMAScript中所有函数的参数都是按值传递的。

什么是按值传递呢?

把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。

按值传递

例如:

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

当传递value到函数foo中,相当于拷贝了一份value,假设拷贝的这份叫 _value,函数中修改的都是 _value 的值,而不会影响原来的 value 值。

共享传递

引用传递,就是传递对象的引用,函数内部对参数的任何改变都会影响该对象的值,因为两者引用的是同一个对象。

例:

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

共享传递是指,在传递对象的时候,传递的是地址索引。

所以修改 o.value,可以通过引用找到原值,但是直接修改 o,并不会修改原值。所以第二个和第三个例子其实都是按共享传递。

参数如果是基本类型是按值传递,如果是引用类型按共享传递。

换句话说,函数传递参数 ,传递的是参数的拷贝:

  1. 指针拷贝,拷贝的是地址索引;
  2. 常规类型拷贝,拷贝的是值 ;

所以,一共是两种传递方式,按值传递和按共享传递。

总结

javascript中数据类型分为基本类型与引用类型:

  1. 基本类型值存储于栈内存中,传递的就是当前值,修改不会影响原有变量的值;
  2. 引用类型值其实也存于栈内存中,只是它的值是指向堆内存当中实际值的一个地址;索引引用传递传的值是栈内存当中的引用地址,当改变时,改变了堆内存当中的实际值;

所以针对上述的内容:

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

内存分布:

改变前:

栈内存堆内存
value1
v1

改变后:

栈内存堆内存
value1
v2

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

改变前:

栈内存堆内存
obj 指针地址{value:1}
o 指针地址{value:1}

改变后:

栈内存堆内存
obj 指针地址{value:2}
o 指针地址{value:2}

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

改变前:

栈内存堆内存
obj 指针地址{value:1}
o 指针地址{value:1}

改变后:

栈内存堆内存
obj 指针地址{value:1}
o 指针地址

手写call和apply

手写call

call() :在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。

let foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call(foo); // 1

注意两点:

  1. call 改变了 this 的指向,指向到 foo
  2. bar 函数执行了;
  • 第一步

    1. 将函数设为对象的属性;

    2. 执行该函数;

    3. 删除该函数;

      //第一版
      Function.prototype.call2 = function(context){
          //首先要获取调用call的函数,用this可以获取
          context.fn = this;
          context.fn();
          delete context.fn;
      }
      
  • 第二步

    call除了可以指定this,还可以指定参数

    //第二版
    Function.prototype.call2 = function(context){
        context.fn = this;
        let arg = [...arguments].slice(1);
        context.fn(...arg);
        delete context.fn;
    }
    
  • 第三步

    1. this 参数可以传 null,当为 null 的时候,视为指向 window

    2. 针对函数,可以实现返回值

      //最终版
      Function.prototype.call2 = function(context,...args){
          //判断context是否是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
      }
      

手写apply

apply 的实现跟 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,之后的一序列参数将会在传递的实参前传入作为它的参数。

bind 函数的两个特点:

  1. 返回一个函数;
  2. 可以传入参数;

返回函数的模拟实现

关于指定 this 的指向,我们可以使用 call 或者 apply 实现

// 第一版
Function.prototype.bind2 = function (context) {
    var self = this;

    // 考虑到绑定函数可能是有返回值的,加上return
    return function () {
        return self.apply(context);
    }

}

传参的模拟实现

当需要传 nameage 两个参数时,可以在 bind 的时候,只传一个 name,在执行返回的函数的时候,再传另一个参数 age

这里如果不适用rest,使用arguments进行处理:

//第二版
Function.prototype.bind2 = function(context){
    var self = this;
    //获取bind2函数从第二个参数开始的所有参数
    var args = Array.prototype.slice.call(arguments,1);
    return function(){
        //这个时候的arguments是指bind返回函数传入的参数
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(context,args.concat(bindArgs));
    }
}

构造函数效果的模拟实现

bind 还有一个特点,就是

一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

也就是说当 bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效。举个例子:

var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}

bar.prototype.friend = 'kevin';

var bindFoo = bar.bind(foo, 'daisy');

var obj = new bindFoo('18');
// undefined
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin

尽管在全局和 foo 中都声明了 value 值,最后依然返回了 undefind,说明绑定的 this 失效了

后文中new 的模拟实现,就会知道这个时候的 this 已经指向了 obj

//第三版
Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        // 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
        // 以上面的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改成 this ,实例会具有 habit 属性
        // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
        return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
    }
    // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
    fBound.prototype = this.prototype;
    return fBound;
}

构造函数效果的优化实现

但是在这个写法中,我们直接将 fBound.prototype = this.prototype,我们直接修改 fBound.prototype 的时候,也会直接修改绑定函数的 prototype。这个时候,我们可以通过一个空函数来进行中转:

// 第四版
Function.prototype.bind2 = function (context) {

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

最终版

Function.prototype.bind2 = function (context) {

    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}
//最终简化版
Function.prototype.myBind = function(context) {
// 判断是否是undefined 和 null
    if (typeof context === "undefined" || context === null) {
    	context = window;
    }
    self = this;
    return function(...args) {
    	return self.apply(context, args);
    }
}

手写模拟new

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例

new 进行的操作:

  1. 创建一个新的空对象
  2. 将空对象的_proto_指向构造函数的原型
  3. 改变this的指向,指向空对象
  4. 对构造函数的返回值做判断,返回对应的值

初步实现

//第一版
function objectFactory(){
    var obj  = new Object();
    Constructor = [].shift.call(arguments);
    obj._proto_ = Constructor.prototype;
    Constructor.apply(obj,arguments);
    return obj;
}

在这一版中,我们:

  1. 用new Object() 的方式新建了一个对象 obj;
  2. 取出第一个参数,就是我们要传入的构造函数。此外因为 shift 会修改原数组,所以 arguments 会被去除第一个参数;
  3. 将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性;
  4. 使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性;
  5. 返回 obj;

最终实现

添加返回值判断

//最终版
function objectFactory(){
    var obj = new Object();
    Constructor = [].shift.acll(arguments);
    obj._proto_ = Constructor.prototype;
    var ret = Constructor.apply(obj,arguments);
    return typeof ret === 'object' ? ret:obj;
}

类数组对象与arguments

类数组对象

拥有一个 length 属性和若干索引属性的对象,无法直接使用原生的数组方法

例:

var array = ['name','age','sex'];
var arrayLike = {
    0:'name',
    1:'age',
    2:'sex',
    length:3
}
读写
console.log(array[0]); // name
console.log(arrayLike[0]); // name

array[0] = 'new name';
arrayLike[0] = 'new name';
长度
console.log(array.length); // 3
console.log(arrayLike.length); // 3
遍历
for(var i = 0, len = array.length; i < len; i++) {
   ……
}
for(var i = 0, len = arrayLike.length; i < len; i++) {
    ……
}
调用数组方法

只能通过Function.call间接调用

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();
}); 
// ["NAME", "AGE", "SEX"]
类数组转数组的方法
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对象

Arguments 对象只定义在函数体中,包括了函数的参数和其他属性。在函数体中,arguments 指代该函数的 Arguments 对象。

length属性

Arguments对象的length属性,表示实参的长度。

例:

function foo(b, c, d){
    console.log("实参的长度为:" + arguments.length)
}

console.log("形参的长度为:" + foo.length)

foo(1)

// 形参的长度为:3
// 实参的长度为:1
callee属性

Arguments 对象的 callee 属性,通过它可以调用函数自身。

闭包经典面试题使用 callee 的解决方法:

var data = [];

for (var i = 0; i < 3; i++) {
    (data[i] = function () {
       console.log(arguments.callee.i) 
    }).i = i;
}

data[0]();
data[1]();
data[2]();

// 0
// 1
// 2
arguments 和对应参数的绑定
function foo(name, age, sex, hobbit) {

    console.log(name, arguments[0]); // name name

    // 改变形参
    name = 'new name';

    console.log(name, arguments[0]); // new name new name

    // 改变arguments
    arguments[1] = 'new age';

    console.log(age, arguments[1]); // new age new age

    // 测试未传入的是否会绑定
    console.log(sex); // undefined

    sex = 'new sex';

    console.log(sex, arguments[2]); // new sex undefined

    arguments[3] = 'new hobbit';

    console.log(hobbit, arguments[3]); // undefined new hobbit

}

foo('name', 'age')

传入的参数,实参和 arguments 的值会共享,当没有传入时,实参与 arguments 值不会共享

传递参数

将参数从一个函数传递到另一个函数

// 使用 apply 将 foo 的参数传递给 bar
function foo() {
    bar.apply(this, arguments);
}
function bar(a, b, c) {
   console.log(a, b, c);
}

foo(1, 2, 3)
转数组

使用ES6的 ... 运算符,可以轻松转成数组。

function func(...arguments) {
    console.log(arguments); // [1, 2, 3]
}

func(1, 2, 3);

补充知识