【准备面试】-JS - 做用域

这期仍是木易大佬的做品,本身复习使用 , 木易老师的博客html

做用域

Javascript中有一个执行上下文execution context的概念,它定义了变量或函数有权访问的其它数据,决定了他们各自的行为。每一个执行环境都有一个与之关联的变量对象,环境中定义的全部变量和函数都保存在这个对象中。node

做用域链

当访问一个变量时,解释器会首先在当前做用域查找标示符,若是没有找到,就去父做用域找,直到找到该变量的标示符或者不在父做用域中,这就是做用域链。git

做用域链和原型继承查找时的区别:若是去查找一个普通对象的属性,可是在当前对象和其原型中都找不到时,会返回undefined;但查找的属性在做用域链中不存在的话就会抛出ReferenceError。github

做用域链的顶端是全局对象,在全局环境中定义的变量就会绑定到全局对象中。数组

无嵌套的函数浏览器

// my_script.js
"use strict";

var foo = 1;
var bar = 2;

function myFunc() {
  
  var a = 1;
  var b = 2;
  var foo = 3;
  console.log("inside myFunc");
  
}

console.log("outside");
myFunc();
复制代码

定义时:当myFunc被定义的时候,myFunc的标识符(identifier)就被加到了全局对象中,这个标识符所引用的是一个函数对象(myFunc function object)。缓存

内部属性[[scope]]指向当前的做用域对象,也就是函数的标识符被建立的时候,咱们所可以直接访问的那个做用域对象(即全局对象)。安全

myFunc所引用的函数对象,其自己不只仅含有函数的代码,而且还含有指向其被建立的时候的做用域对象。bash

调用时:当myFunc函数被调用的时候,一个新的做用域对象被建立了。新的做用域对象中包含myFunc函数所定义的本地变量,以及其参数(arguments)。这个新的做用域对象的父做用域对象就是在运行myFunc时能直接访问的那个做用域对象(即全局对象)。数据结构

有嵌套的函数

当函数返回没有被引用的时候,就会被垃圾回收器回收。可是对于闭包,即便外部函数返回了,函数对象仍会引用它被建立时的做用域对象。

"use strict";
function createCounter(initial) {
  var counter = initial;
  
  function increment(value) {
    counter += value;
  }
  
  function get() {
    return counter;
  }
  
  return {
    increment: increment,
    get: get
  };
}

var myCounter = createCounter(100);
console.log(myCounter.get());   // 返回 100

myCounter.increment(5);
console.log(myCounter.get());   // 返回 105
复制代码

当调用 createCounter(100) 时,内嵌函数increment和get都有指向createCounter(100) scope的引用。假设createCounter(100)没有任何返回值,那么createCounter(100) scope再也不被引用,因而就能够被垃圾回收。

可是createCounter(100)其实是有返回值的,而且返回值被存储在了myCounter中,因此对象之间的引用关系以下图:
即便 createCounter(100)已经返回,可是其做用域仍在,而且只能被内联函数访问。能够经过调用 myCounter.increment()myCounter.get()来直接访问 createCounter(100)的做用域。

myCounter.increment()myCounter.get()被调用时,新的做用域对象会被建立,而且该做用域对象的父做用域对象会是当前能够直接访问的做用域对象。

调用get()时,当执行到return counter时,在get()所在的做用域并无找到对应的标示符,就会沿着做用域链往上找,直到找到变量counter,而后返回该变量。

单独调用 increment(5)时,参数 value保存在当前的做用域对象。当函数要访问 counter时,没有找到,因而沿着做用域链向上查找,在 createCounter(100)的做用域找到了对应的标示符, increment()就会修改 counter的值。除此以外,没有其余方式来修改这个变量。闭包的强大也在于此,可以存贮私有数据。

建立两个函数:myCounter1和myCounter2

//my_script.js
"use strict";
function createCounter(initial) {
  /* ... see the code from previous example ... */
}

//-- create counter objects
var myCounter1 = createCounter(100);
var myCounter2 = createCounter(200);
复制代码

myCounter1.increment和myCounter2.increment的函数对象拥有着同样的代码以及同样的属性值(name,length等等),可是它们的[[scope]]指向的是不同的做用域对象。

this

调用位置

调用位置就是函数在代码中被调用的位置(而不是声明的位置)。

查找方法:

  • 分析调用栈:调用位置就是当前正在执行的函数的前一个调用中
function baz() {
    // 当前调用栈是:baz
    // 所以,当前调用位置是全局做用域
    
    console.log( "baz" );
    bar(); // <-- bar的调用位置
}

function bar() {
    // 当前调用栈是:baz --> bar
    // 所以,当前调用位置在baz中
    
    console.log( "bar" );
    foo(); // <-- foo的调用位置
}

function foo() {
    // 当前调用栈是:baz --> bar --> foo
    // 所以,当前调用位置在bar中
    
    console.log( "foo" );
}
baz(); // <-- baz的调用位置
复制代码
  • 使用开发者工具获得调用栈: 设置断点或者插入debugger;语句,运行时调试器会在那个位置暂停,同时展现当前位置的函数调用列表,这就是调用栈。找到栈中的第二个元素,这就是真正的调用位置。

绑定规则

默认绑定:

  • 独立函数调用,能够把默认绑定看做是没法应用其余规则时的默认规则,this指向全局对象。
  • 严格模式下,不能将全局对象用于默认绑定,this会绑定到undefined。只有函数运行在非严格模式下,默认绑定才能绑定到全局对象。在严格模式下调用函数则不影响默认绑定。
function foo() { // 运行在严格模式下,this会绑定到undefined
    "use strict";
    
    console.log( this.a );
}

var a = 2;

// 调用
foo(); // TypeError: Cannot read property 'a' of undefined

// --------------------------------------

function foo() { // 运行
    console.log( this.a );
}

var a = 2;

(function() { // 严格模式下调用函数则不影响默认绑定
    "use strict";
    
    foo(); // 2
})();
复制代码

隐式绑定:

当函数引用有上下文对象时,隐式绑定规则会把函数中的this绑定到这个上下文对象。对象属性引用链中只有上一层或者说最后一层在调用中起做用。

function foo() {
    console.log( this.a );
}

var obj = {
    a: 2,
    foo: foo
};

obj.foo(); // 2
复制代码

隐式丢失

被隐式绑定的函数特定状况下会丢失绑定对象,应用默认绑定,把this绑定到全局对象或者undefined上。

// 虽然bar是obj.foo的一个引用,可是实际上,它引用的是foo函数自己。
// bar()是一个不带任何修饰的函数调用,应用默认绑定。
function foo() {
    console.log( this.a );
}

var obj = {
    a: 2,
    foo: foo
};

var bar = obj.foo; // 函数别名

var a = "oops, global"; // a是全局对象的属性

bar(); // "oops, global"
复制代码

参数传递就是一种隐式赋值,传入函数时也会被隐式赋值。回调函数丢失this绑定是很是常见的。

function foo() {
    console.log( this.a );
}

function doFoo(fn) {
    // fn其实引用的是foo
    
    fn(); // <-- 调用位置!
}

var obj = {
    a: 2,
    foo: foo
};

var a = "oops, global"; // a是全局对象的属性

doFoo( obj.foo ); // "oops, global"

// ----------------------------------------

// JS环境中内置的setTimeout()函数实现和下面的伪代码相似:
function setTimeout(fn, delay) {
    // 等待delay毫秒
    fn(); // <-- 调用位置!
}
复制代码

显示绑定

经过call(..) 或者 apply(..)方法。第一个参数是一个对象,在调用函数时将这个对象绑定到this。由于直接指定this的绑定对象,称之为显示绑定。

function foo() {
    console.log( this.a );
}

var obj = {
    a: 2
};

foo.call( obj ); // 2  调用foo时强制把foo的this绑定到obj上
复制代码

显示绑定没法解决丢失绑定问题。

解决方案:

一、硬绑定 建立函数bar(),并在它的内部手动调用foo.call(obj),强制把foo的this绑定到了obj。

function foo() {
    console.log( this.a );
}

var obj = {
    a: 2
};

var bar = function() {
    foo.call( obj );
};

bar(); // 2
setTimeout( bar, 100 ); // 2

// 硬绑定的bar不可能再修改它的this
bar.call( window ); // 2
复制代码

典型应用场景是建立一个包裹函数,负责接收参数并返回值。

function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}

var obj = {
    a: 2
};

var bar = function() {
    return foo.apply( obj, arguments );
};

var b = bar( 3 ); // 2 3
console.log( b ); // 5
复制代码

建立一个能够重复使用的辅助函数。

function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}

// 简单的辅助绑定函数
function bind(fn, obj) {
    return function() {
        return fn.apply( obj, arguments );
    }
}

var obj = {
    a: 2
};

var bar = bind( foo, obj );

var b = bar( 3 ); // 2 3
console.log( b ); // 5
复制代码

ES5内置了Function.prototype.bind,bind会返回一个硬绑定的新函数,用法以下。

function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}

var obj = {
    a: 2
};

var bar = foo.bind( obj );

var b = bar( 3 ); // 2 3
console.log( b ); // 5
复制代码

二、API调用的“上下文”

JS许多内置函数提供了一个可选参数,被称之为“上下文”(context),其做用和bind(..)同样,确保回调函数使用指定的this。这些函数实际上经过call(..)和apply(..)实现了显式绑定。

function foo(el) {
	console.log( el, this.id );
}

var obj = {
    id: "awesome"
}

var myArray = [1, 2, 3]
// 调用foo(..)时把this绑定到obj
myArray.forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome
复制代码

new绑定

在JS中,构造函数只是使用new操做符时被调用的普通函数,他们不属于某个类,也不会实例化一个类。 包括内置对象函数(好比Number(..))在内的全部函数均可以用new来调用,这种函数调用被称为构造函数调用。 实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。 使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操做。

  • 一、建立(或者说构造)一个新对象。
  • 二、这个新对象会被执行[[Prototype]]链接。
  • 三、这个新对象会绑定到函数调用的this。
  • 四、若是函数没有返回其余对象,那么new表达式中的函数调用会自动返回这个新对象。 使用new来调用foo(..)时,会构造一个新对象并把它(bar)绑定到foo(..)调用中的this。
function foo(a) {
    this.a = a;
}

var bar = new foo(2); // bar和foo(..)调用中的this进行绑定
console.log( bar.a ); // 2
复制代码

绑定例外

1.被忽略的this

null或者undefined做为this的绑定对象传入callapply或者bind,这些值在调用时会被忽略,实际应用的是默认规则。

下面两种状况下会传入null

  • 使用apply(..)来“展开”一个数组,并看成参数传入一个函数
  • bind(..)能够对参数进行柯里化(预先设置一些参数)
function foo(a, b) {
    console.log( "a:" + a + ",b:" + b );
}

// 把数组”展开“成参数
foo.apply( null, [2, 3] ); // a:2,b:3

// 使用bind(..)进行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2,b:3 
复制代码

老是传入null来忽略this绑定可能产生一些反作用。若是某个函数确实使用了this,那默认绑定规则会把this绑定到全局对象中。

更安全的this

安全的作法就是传入一个特殊的对象(空对象),把this绑定到这个对象不会对你的程序产生任何反作用。

JS中建立一个空对象最简单的方法是Object.create(null),这个和{}很像,可是并不会建立Object.prototype这个委托,因此比{ }更空。

function foo(a, b) {
    console.log( "a:" + a + ",b:" + b );
}

// 咱们的空对象
var ø = Object.create( null );

// 把数组”展开“成参数
foo.apply( ø, [2, 3] ); // a:2,b:3

// 使用bind(..)进行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2,b:3 
复制代码
  1. 间接引用

间接引用下,调用这个函数会应用默认绑定规则。间接引用最容易在赋值时发生。

// p.foo = o.foo的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或者o.foo()
function foo() {
    console.log( this.a );
}

var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4};

o.foo(); // 3
(p.foo = o.foo)(); // 2

复制代码
  1. 软绑定
  • 硬绑定能够把this强制绑定到指定的对象(new除外),防止函数调用应用默认绑定规则。可是会下降函数的灵活性,使用硬绑定以后就没法使用隐式绑定或者显式绑定来修改this
  • 若是给默认绑定指定一个全局对象和undefined之外的值,那就能够实现和硬绑定相同的效果,同时保留隐式绑定或者显示绑定修改this的能力。
// 默认绑定规则,优先级排最后
// 若是this绑定到全局对象或者undefined,那就把指定的默认对象obj绑定到this,不然不会修改this
if(!Function.prototype.softBind) {
    Function.prototype.softBind = function(obj) {
        var fn = this;
        // 捕获全部curried参数
        var curried = [].slice.call( arguments, 1 ); 
        var bound = function() {
            return fn.apply(
            	(!this || this === (window || global)) ? 
                	obj : this,
                curried.concat.apply( curried, arguments )
            );
        };
        bound.prototype = Object.create( fn.prototype );
        return bound;
    };
}
复制代码

使用:软绑定版本的foo()能够手动将this绑定到obj2或者obj3上,但若是应用默认绑定,则会将this绑定到obj。

function foo() {
    console.log("name:" + this.name);
}

var obj = { name: "obj" },
    obj2 = { name: "obj2" },
    obj3 = { name: "obj3" };

// 默认绑定,应用软绑定,软绑定把this绑定到默认对象obj
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj 

// 隐式绑定规则
obj2.foo = foo.softBind( obj );
obj2.foo(); // name: obj2 <---- 看!!!

// 显式绑定规则
fooOBJ.call( obj3 ); // name: obj3 <---- 看!!!

// 绑定丢失,应用软绑定
setTimeout( obj2.foo, 10 ); // name: obj
复制代码
  1. 箭头函数

根据外层(函数或者全局)做用域(词法做用域)来决定this

  • 箭头函数不绑定this,箭头函数中的this至关于普通变量。
  • 箭头函数的this寻值行为与普通变量相同,在做用域中逐级寻找。
  • 箭头函数的this没法经过bind,call,apply来直接修改(能够间接修改)。
  • 改变做用域中this的指向能够改变箭头函数的this
  • eg. function closure(){()=>{//code }},在此例中,咱们经过改变封包环境closure.bind(another)(),来改变箭头函数this的指向。
/**
 * 非严格模式
 */

var name = 'window'

var person1 = {
  name: 'person1',
  show1: function () {
    console.log(this.name)
  },
  show2: () => console.log(this.name),
  show3: function () {
    return function () {
      console.log(this.name)
    }
  },
  show4: function () {
    return () => console.log(this.name)
  }
}
var person2 = { name: 'person2' }

person1.show1()
person1.show1.call(person2)

person1.show2()
person1.show2.call(person2)

person1.show3()()
person1.show3().call(person2)
person1.show3.call(person2)()

person1.show4()()
person1.show4().call(person2)
person1.show4.call(person2)()
复制代码


正确答案以下:

person1.show1() // person1,隐式绑定,this指向调用者 person1 
person1.show1.call(person2) // person2,显式绑定,this指向 person2

person1.show2() // window,箭头函数绑定,this指向外层做用域,即全局做用域
person1.show2.call(person2) // window,箭头函数绑定,this指向外层做用域,即全局做用域

person1.show3()() // window,默认绑定,这是一个高阶函数,调用者是window
				  // 相似于`var func = person1.show3()` 执行`func()`
person1.show3().call(person2) // person2,显式绑定,this指向 person2
person1.show3.call(person2)() // window,默认绑定,调用者是window

person1.show4()() // person1,箭头函数绑定,this指向外层做用域,即person1函数做用域
person1.show4().call(person2) // person1,箭头函数绑定,
							  // this指向外层做用域,即person1函数做用域
person1.show4.call(person2)() // person2
复制代码

此次经过构造函数来建立一个对象,并执行相同的4个show方法。

/**
 * 非严格模式
 */

var name = 'window'

function Person (name) {
  this.name = name;
  this.show1 = function () {
    console.log(this.name)
  }
  this.show2 = () => console.log(this.name)
  this.show3 = function () {
    return function () {
      console.log(this.name)
    }
  }
  this.show4 = function () {
    return () => console.log(this.name)
  }
}

var personA = new Person('personA')
var personB = new Person('personB')

personA.show1()
personA.show1.call(personB)

personA.show2()
personA.show2.call(personB)

personA.show3()()
personA.show3().call(personB)
personA.show3.call(personB)()

personA.show4()()
personA.show4().call(personB)
personA.show4.call(personB)()
复制代码


正确答案以下:

personA.show1() // personA,隐式绑定,调用者是 personA
personA.show1.call(personB) // personB,显式绑定,调用者是 personB

personA.show2() // personA,首先personA是new绑定,产生了新的构造函数做用域,
			// 而后是箭头函数绑定,this指向外层做用域,即personA函数做用域
personA.show2.call(personB) // personA,同上

personA.show3()() // window,默认绑定,调用者是window
personA.show3().call(personB) // personB,显式绑定,调用者是personB
personA.show3.call(personB)() // window,默认绑定,调用者是window

personA.show4()() // personA,箭头函数绑定,this指向外层做用域,即personA函数做用域
personA.show4().call(personB) // personA,箭头函数绑定,call并无改变外层做用域,
			     // this指向外层做用域,即personA函数做用域
personA.show4.call(personB)() // personB,解析同题目1,最后是箭头函数绑定,
			     // this指向外层做用域,即改变后的person2函数做用域
复制代码

题目一和题目二的区别在于题目二使用了new操做符。

使用 new 操做符调用构造函数,实际上会经历一下4个步骤:

  • 建立一个新对象;
  • 将构造函数的做用域赋给新对象(所以this就指向了这个新对象);
  • 执行构造函数中的代码(为这个新对象添加属性);
  • 返回新对象。

深度解析 call 和 apply

call()apply()的区别在于,call()方法接受的是若干个参数的列表,而apply()方法接受的是一个包含多个参数的数组

举个例子:

var func = function(arg1, arg2) {
     ...
};

func.call(this, arg1, arg2); // 使用 call,参数列表
func.apply(this, [arg1, arg2]) // 使用 apply,参数数组
复制代码

使用场景

  • 合并两个数组
var vegetables = ['parsnip', 'potato'];
var moreVegs = ['celery', 'beetroot'];

// 将第二个数组融合进第一个数组
// 至关于 vegetables.push('celery', 'beetroot');
Array.prototype.push.apply(vegetables, moreVegs);
// 4

vegetables;
// ['parsnip', 'potato', 'celery', 'beetroot']
复制代码

当第二个数组(如示例中的 moreVegs)太大时不要使用这个方法来合并数组,由于一个函数可以接受的参数个数是有限制的。不一样的引擎有不一样的限制,JS核心限制在 65535,有些引擎会抛出异常,有些不抛出异常但丢失多余参数。

如何解决呢?方法就是将参数数组切块后循环传入目标方法

function concatOfArray(arr1, arr2) {
    var QUANTUM = 32768;
    for (var i = 0, len = arr2.length; i < len; i += QUANTUM) {
        Array.prototype.push.apply(
            arr1, 
            arr2.slice(i, Math.min(i + QUANTUM, len) )
        );
    }
    return arr1;
}

// 验证代码
var arr1 = [-3, -2, -1];
var arr2 = [];
for(var i = 0; i < 1000000; i++) {
    arr2.push(i);
}

Array.prototype.push.apply(arr1, arr2);
// Uncaught RangeError: Maximum call stack size exceeded

concatOfArray(arr1, arr2);
// (1000003) [-3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
复制代码
  • 获取数组中的最大值和最小值
var numbers = [5, 458 , 120 , -215 ]; 
Math.max.apply(Math, numbers);   //458    
Math.max.call(Math, 5, 458 , 120 , -215); //458

// ES6
Math.max.call(Math, ...numbers); // 458
复制代码

为何要这么用呢,由于数组 numbers自己没有max 方法,可是 Math 有呀,因此这里就是借助 call / apply 使用Math.max 方法。

  • 数组对象(Array-like Object)使用数组方法
var domNodes = document.getElementsByTagName("*");
domNodes.unshift("h1");
// TypeError: domNodes.unshift is not a function

var domNodeArrays = Array.prototype.slice.call(domNodes);
domNodeArrays.unshift("h1"); // 505 不一样环境下数据不一样
// (505) ["h1", html.gr__hujiang_com, head, meta, ...] 
复制代码

类数组对象有下面两个特性

  • 一、具备:指向对象元素的数字索引下标和 length属性
  • 二、不具备:好比push 、shift、 forEach以及 indexOf 等数组对象具备的方法 要说明的是,类数组对象是一个对象。JS中存在一种名为类数组的对象结构,好比 arguments 对象,还有DOM API 返回的NodeList 对象都属于类数组对象,类数组对象不能使用 push/pop/shift/unshift 等数组方法,经过 Array.prototype.slice.call 转换成真正的数组,就可使用 Array下全部方法。

类数组对象转数组的其余方法:

var arr = [].slice.call(arguments);

ES6:
let arr = Array.from(arguments);
let arr = [...arguments];
复制代码

Array.from()能够将两类对象转为真正的数组:类数组对象和可遍历(iterable)对象(包括ES6新增的数据结构 Set 和 Map)。

PS扩展一:为何经过 Array.prototype.slice.call() 就能够把类数组对象转换成数组?

其实很简单,sliceArray-like 对象经过下标操做放进了新的 Array 里面。

下面代码是 MDN 关于 slicePolyfill,连接 Array.prototype.slice()

Array.prototype.slice = function(begin, end) {
      end = (typeof end !== 'undefined') ? end : this.length;

      // For array like object we handle it ourselves.
      var i, cloned = [],
        size, len = this.length;

      // Handle negative value for "begin"
      var start = begin || 0;
      start = (start >= 0) ? start : Math.max(0, len + start);

      // Handle negative value for "end"
      var upTo = (typeof end == 'number') ? Math.min(end, len) : len;
      if (end < 0) {
        upTo = len + end;
      }

      // Actual expected size of the slice
      size = upTo - start;

      if (size > 0) {
        cloned = new Array(size);
        if (this.charAt) {
          for (i = 0; i < size; i++) {
            cloned[i] = this.charAt(start + i);
          }
        } else {
          for (i = 0; i < size; i++) {
            cloned[i] = this[start + i];
          }
        }
      }

      return cloned;
    };
  }
复制代码

PS扩展二:经过 Array.prototype.slice.call() 就足够了吗?存在什么问题?

在低版本IE下不支持经过Array.prototype.slice.call(args)将类数组对象转换成数组,由于低版本IE(IE < 9)下的DOM对象是以 com 对象的形式实现的,js对象与 com 对象不能进行转换。

兼容写法以下:

function toArray(nodes){
    try {
        // works in every browser except IE
        return Array.prototype.slice.call(nodes);
    } catch(err) {
        // Fails in IE < 9
        var arr = [],
            length = nodes.length;
        for(var i = 0; i < length; i++){
            // arr.push(nodes[i]); // 两种均可以
            arr[i] = nodes[i];
        }
        return arr;
    }
}
复制代码

PS 扩展三:为何要有类数组对象呢?或者说类数组对象是为何解决什么问题才出现的?

JavaScript类型化数组是一种相似数组的对象,并提供了一种用于访问原始二进制数据的机制。 Array存储的对象能动态增多和减小,而且能够存储任何JavaScript值。JavaScript引擎会作一些内部优化,以便对数组的操做能够很快。然而,随着Web应用程序变得愈来愈强大,尤为一些新增长的功能例如:音频视频编辑,访问WebSockets的原始数据等,很明显有些时候若是使用JavaScript代码能够快速方便地经过类型化数组来操做原始的二进制数据,这将会很是有帮助。

一句话就是,能够更快的操做复杂数据。

  • 调用父构造函数实现继承
function  SuperType(){
    this.color=["red", "green", "blue"];
}
function  SubType(){
    // 核心代码,继承自SuperType
    SuperType.call(this);
}

var instance1 = new SubType();
instance1.color.push("black");
console.log(instance1.color);
// ["red", "green", "blue", "black"]

var instance2 = new SubType();
console.log(instance2.color);
// ["red", "green", "blue"]
复制代码

在子构造函数中,经过调用父构造函数的call方法来实现继承,因而SubType的每一个实例都会将SuperType 中的属性复制一份。

缺点: 只能继承父类的实例属性和方法,不能继承原型属性/方法 没法实现复用,每一个子类都有父类实例函数的副本,影响性能

call的模拟实现

先看下面一个简单的例子

var value = 0;
var foo = {
    value: 1
};

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

bar.call(foo); // 1
复制代码

经过上面的介绍咱们知道,call()主要有如下两点

  • 一、call()改变了this的指向
  • 二、函数 bar 执行了 模拟实现第一步 若是在调用call()的时候把函数bar()添加到foo()对象中,即以下
var foo = {
    value: 1,
    bar: function() {
        console.log(this.value);
    }
};

foo.bar(); // 1
复制代码

这个改动就能够实现:改变了this的指向而且执行了函数bar

可是这样写是有反作用的,即给foo额外添加了一个属性,怎么解决呢?

解决方法很简单,用 delete 删掉就行了。

因此只要实现下面3步就能够模拟实现了。

  • 一、将函数设置为对象的属性:foo.fn = bar
  • 二、执行函数:foo.fn()
  • 三、删除函数:delete foo.fn 代码实现以下:
// 初版
Function.prototype.call2 = function(context) {
    // 首先要获取调用call的函数,用this能够获取
    context.fn = this; 		// foo.fn = bar
    context.fn();			// foo.fn()
    delete context.fn;		// delete foo.fn
}

// 测试一下
var foo = {
    value: 1
};

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

bar.call2(foo); // 1
复制代码

完美!

  • 模拟实现第二步

初版有一个问题,那就是函数 bar 不能接收参数,因此咱们能够从 arguments中获取参数,取出第二个到最后一个参数放到数组中,为何要抛弃第一个参数呢,由于第一个参数是 this

类数组对象转成数组的方法上面已经介绍过了,可是这边使用ES3的方案来作。

var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
    args.push('arguments[' + i + ']');
}
复制代码

参数数组搞定了,接下来要作的就是执行函数 context.fn()

context.fn( args.join(',') ); // 这样不行 上面直接调用确定不行,args.join(',')会返回一个字符串,并不会执行。

这边采用 eval方法来实现,拼成一个函数。

eval('context.fn(' + args +')') 上面代码中args 会自动调用 args.toString() 方法,由于'context.fn(' + args +')'本质上是字符串拼接,会自动调用toString()方法,以下代码:

var args = ["a1", "b2", "c3"];
console.log(args);
// ["a1", "b2", "c3"]

console.log(args.toString());
// a1,b2,c3

console.log("" + args);
// a1,b2,c3
复制代码

因此说第二个版本就实现了,代码以下:

// 第二版
Function.prototype.call2 = function(context) {
    context.fn = this;
    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    eval('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); 
// kevin
// 18
// 1
完美!!
复制代码
  • 模拟实现第三步

还有2个细节须要注意:

  1. this 参数能够传 null 或者 undefined,此时 this 指向 window
  2. this 参数能够传基本类型数据,原生的 call 会自动用 Object() 转换
  3. 函数是能够有返回值的 实现上面的三点很简单,代码以下
// 第三版
Function.prototype.call2 = function (context) {
    context = context ? Object(context) : window; // 实现细节 1 和 2
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }

    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result; // 实现细节 2
}

// 测试一下
var value = 2;

var obj = {
    value: 1
}

function bar(name, age) {
    console.log(this.value);
    return {
        value: this.value,
        name: name,
        age: age
    }
}

function foo() {
    console.log(this);
}

bar.call2(null); // 2
foo.call2(123); // Number {123, fn: ƒ}

bar.call2(obj, 'kevin', 18);
// 1
// {
//    value: 1,
//    name: 'kevin',
//    age: 18
// }
完美!!!
复制代码

call和apply模拟实现汇总

call的模拟实现

ES3:

Function.prototype.call = function (context) {
    context = context ? Object(context) : window; 
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result;
}
复制代码

ES6:

Function.prototype.call = function (context) {
  context = context ? Object(context) : window; 
  context.fn = this;

  let args = [...arguments].slice(1);
  let result = context.fn(...args);

  delete context.fn
  return result;
}
复制代码

apply的模拟实现

ES3:

Function.prototype.apply = function (context, arr) {
    context = 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;
}
复制代码

ES6:

Function.prototype.apply = function (context, arr) {
    context = context ? Object(context) : window; 
    context.fn = this;
  
    let result;
    if (!arr) {
        result = context.fn();
    } else {
        result = context.fn(...arr);
    }
      
    delete context.fn
    return result;
}
复制代码

bind

bind()方法会建立一个新函数,当这个新函数被调用时,它的 this 值是传递给 bind() 的第一个参数,传入bind方法的第二个以及之后的参数加上绑定函数运行时自己的参数按照顺序做为原函数的参数来调用原函数。bind返回的绑定函数也能使用 new 操做符建立对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

bind方法与 call / apply 最大的不一样就是前者返回一个绑定上下文的函数,然后二者是直接执行了函数。

来个例子说明下

var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    return {
		value: this.value,
		name: name,
		age: age
    }
};

bar.call(foo, "Jack", 20); // 直接执行了函数
// {value: 1, name: "Jack", age: 20}

var bindFoo1 = bar.bind(foo, "Jack", 20); // 返回一个函数
bindFoo1();
// {value: 1, name: "Jack", age: 20}

var bindFoo2 = bar.bind(foo, "Jack"); // 返回一个函数
bindFoo2(20);
// {value: 1, name: "Jack", age: 20}
复制代码

经过上述代码能够看出bind 有以下特性:

  • 一、能够指定this
  • 二、返回一个函数
  • 三、能够传入参数
  • 四、柯里化

使用场景

一、业务场景

常常有以下的业务场景

var nickname = "Kitty";
function Person(name){
    this.nickname = name;
    this.distractedGreeting = function() {

        setTimeout(function(){
            console.log("Hello, my name is " + this.nickname);
        }, 500);
    }
}
 
var person = new Person('jawil');
person.distractedGreeting();
//Hello, my name is Kitty
复制代码

这里输出的nickname是全局的,并非咱们建立 person时传入的参数,由于 setTimeout 在全局环境中执行,因此 this 指向的是window。

这边把setTimeout 换成异步回调也是同样的,好比接口请求回调。

解决方案有下面两种。

解决方案1:缓存 this

var nickname = "Kitty";
function Person(name){
    this.nickname = name;
    this.distractedGreeting = function() {
        
		var self = this; // added
        setTimeout(function(){
            console.log("Hello, my name is " + self.nickname); // changed
        }, 500);
    }
}
 
var person = new Person('jawil');
person.distractedGreeting();
// Hello, my name is jawil
复制代码

解决方案2:使用 bind

var nickname = "Kitty";
function Person(name){
    this.nickname = name;
    this.distractedGreeting = function() {

        setTimeout(function(){
            console.log("Hello, my name is " + this.nickname);
        }.bind(this), 500);
    }
}
 
var person = new Person('jawil');
person.distractedGreeting();
// Hello, my name is jawil
复制代码

完美!

二、柯里化(curry)

只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

能够一次性地调用柯里化函数,也能够每次只传一个参数分屡次调用。

var add = function(x) {
  return function(y) {
    return x + y;
  };
};

var increment = add(1);
var addTen = add(10);

increment(2);
// 3

addTen(2);
// 12

add(1)(2);
// 3
复制代码

这里定义了一个add 函数,它接受一个参数并返回一个新的函数。调用 add 以后,返回的函数就经过闭包的方式记住了 add的第一个参数。因此说 bind 自己也是闭包的一种使用场景。

模拟实现

bind() 函数在ES5 才被加入,因此并非全部浏览器都支持,IE8及如下的版本中不被支持,若是须要兼容可使用Polyfill 来实现。

首先咱们来实现如下四点特性:

  • 一、能够指定this
  • 二、返回一个函数
  • 三、能够传入参数
  • 四、柯里化

模拟实现第一步 对于第 1 点,使用 call / apply指定 this

对于第 2 点,使用 return 返回一个函数。

结合前面 2 点,能够写出初版,代码以下:

// 初版
Function.prototype.bind2 = function(context) {
    var self = this; // this 指向调用者
    return function () { // 实现第 2点
        return self.apply(context); // 实现第 1 点
    }
}
测试一下

// 测试用例
var value = 2;
var foo = {
    value: 1
};

function bar() {
	return this.value;
}

var bindFoo = bar.bind2(foo);

bindFoo(); // 1
复制代码

模拟实现第二步 对于第 3 点,使用 arguments 获取参数数组并做为 self.apply() 的第二个参数。

对于第 4 点,获取返回函数的参数,而后同第3点的参数合并成一个参数数组,并做为 self.apply() 的第二个参数。

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

    var self = this;
    // 实现第3点,由于第1个参数是指定的this,因此只截取第1个以后的参数
	// arr.slice(begin); 即 [begin, end]
    var args = Array.prototype.slice.call(arguments, 1); 

    return function () {
        // 实现第4点,这时的arguments是指bind返回的函数传入的参数
        // 即 return function 的参数
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply( context, args.concat(bindArgs) );
    }
}
测试一下:

// 测试用例
var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    return {
		value: this.value,
		name: name,
		age: age
    }
};

var bindFoo = bar.bind2(foo, "Jack");
bindFoo(20);
// {value: 1, name: "Jack", age: 20}
复制代码

模拟实现第三步 到如今已经完成大部分了,可是还有一个难点,bind有如下一个特性

一个绑定函数也能使用new操做符建立对象:这种行为就像把原函数当成构造器,提供的 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, 'Jack');
var obj = new bindFoo(20);
// undefined
// Jack
// 20

obj.habit;
// shopping

obj.friend;
// kevin
复制代码

上面例子中,运行结果this.value 输出为 undefined,这不是全局value 也不是foo对象中的value,这说明 bindthis 对象失效了,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);
        
        // 注释1
        return self.apply(
            this instanceof fBound ? this : context, 
            args.concat(bindArgs)
        );
    }
    // 注释2
    fBound.prototype = this.prototype;
    return fBound;
}
复制代码

注释1: 看成为构造函数时,this指向实例,此时 this instanceof fBound 结果为 true,可让实例得到来自绑定函数的值,即上例中实例会具备 habit 属性。 看成为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context 注释2: 修改返回函数的 prototype 为绑定函数的 prototype,实例就能够继承绑定函数的原型中的值,即上例中obj能够获取到 bar 原型上的 friend

模拟实现第四步 上面实现中 fBound.prototype = this.prototype有一个缺点,直接修改 fBound.prototype 的时候,也会直接修改this.prototype

来个代码测试下:

// 测试用例
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.bind2(foo, 'Jack'); // bind2
var obj = new bindFoo(20); // 返回正确
// undefined
// Jack
// 20

obj.habit; // 返回正确
// shopping

obj.friend; // 返回正确
// kevin

obj.__proto__.friend = "Kitty"; // 修改原型

bar.prototype.friend; // 返回错误,这里被修改了
// Kitty
复制代码

解决方案是用一个空对象做为中介,把 fBound.prototype 赋值为空对象的实例(原型式继承)。

var fNOP = function () {};			// 建立一个空对象
fNOP.prototype = this.prototype; 	// 空对象的原型指向绑定函数的原型
fBound.prototype = new fNOP();		// 空对象的实例赋值给 fBound.prototype
这边能够直接使用ES5的 Object.create()方法生成一个新对象

fBound.prototype = Object.create(this.prototype);
复制代码

不过bindObject.create()都是ES5方法,部分IE浏览器(IE < 9)并不支持,Polyfill中不能用 Object.create()实现bind,不过原理是同样的。

第四版目前OK啦,代码以下:

// 第四版,已经过测试用例
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;
}
复制代码

模拟实现第五步 到这里其实已经差很少了,但有一个问题是调用 bind 的不是函数,这时候须要抛出异常。

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

因此完整版模拟实现代码以下:

// 第五版
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;
}
复制代码
相关文章
相关标签/搜索