第二节 函数与指针


代码,是前端工程师的“武器”,也是他们的“面包和黄油”。


一、变量

// 关键词:变量 指针 引用 地址
// 参考:https://www.cnblogs.com/fitzlovecode/p/jsadvanced.html

二、普通函数

1. 语法

function fun(){
  console.log('我是普通函数')
}

// 1-多个形参:...rest参数 rest参数只能写在最后
// 2-参数访问:arguments伪数组对象(箭头函数不可用) arguments.length  arguments[i]

// call()与apply() call() 和 apply()都是为了改变被调用的函数内部的上下文(context --执行上下文)而存在的,即改变被调用的函数内部的this指向;

// call() 和 apply()二者的作用完全一样,只是接受参数的方式不同,call接收参数列表,apply接收一个数组或伪数组;

//1-call() 
function.call(thisArg, arg1, arg2, arg...);

语法参数说明:call()方法调用一个函数function, 其具有一个指定的this值和分别提供的参数
作用:可以让call()中的对象调用当前对象所拥有的函数
call()中的对象指的是:thisArg,即传递到call()方法中的this
当前对象指的是:function对象
区别:和apply()的区别在于传递的参数不同,apply传递的参数是一个数组或伪数组
非严格模式中: thisnullundefined时会自动指向全局对象(window对象)

//2-apply() 
function.apply(thisArg, [argsArray]);

语法参数说明:apply()方法调用一个函数function, 其具有一个指定的this值和数组或伪数组
作用:可以让apply()中的对象调用当前对象所拥有的函数
apply()中的对象指的是:thisArg,即传递到apply()方法中的this
当前对象指的是:function对象

区别:和call()的区别在于传递的参数不同,call()传递的参数是一个个的参数
非严格模式中: thisnullundefined时会自动指向全局对象(window对象)
Chrome 14 以及 Internet Explorer 9 仍然不接受伪数组对象。如果传入伪数组对象,它们会抛出异常

2. 参数

函数的参数分为形参和实参,形参是函数被定义时在函数名后的括号内声明的参数,实参则是在调用时向函数传递的实际参数。注意形参与实参并不一定要个数相等,不需要严格一一对应。当实参的数量大于形参的数量时,实参会被函数内部的类数组——实参列表arguments保存。实参和实参列表有对应关系时是保持映射关系的(不是同一个),如以下情况,但是没有对应关系时就没有映射关系。

arguments 是个类数组对象,其包含一个 length 属性,可以用 arguments.length 来获得传入函数的参数个数。

function printArgs() {
    console.log(arguments);
}
printArgs("A", "a", 0, { foo: "Hello, arguments" });

执行结果:
["A", "a", 0, Object]
// [Arguments] { '0': 10, '1': 20, '2': 30, '3': 40, '4': 50 }
// 类数组对象中(长的像是一个数组, 本质上是一个对象: arguments

区别:满足一定条件的对象都能被 slice 方法转换成数组
const obj = { 0: "A", 1: "B", length: 2 };
条件就是: 
1) 属性为 012...2)具有 length 属性;

事实上,Object 与 Array 的唯一区别就是 Object 的属性是 string,而 Array 的索引是 number。

伪数组的特性就是长得像数组,包含一组数据以及拥有一个 length 属性,但是没有任何 Array 的方法。再具体的说,length 属性是个非负整数,上限是 JavaScript 中能精确表达的最大数字;另外,类数组对象的 length 值无法自动改变。

在这里插入图片描述
在这里插入图片描述

 // arguments 转换成数组
const args = Array.prototype.slice.call(arguments);
// or
const args = [].slice.call(arguments)
// or
const args = Array.from(arguments);
// or
const args = [...arguments];

3. 函数的返回值(停止条件)

return可以将函数执行的结果返回被调用的地方,同时停止函数的执行。

4. 作用域

全局作用域:不在任何函数内定义的变量就具有全局作用域。实际上,JavaScript默认有一个全局对象window,全局作用域的变量实际上被绑定到window的一个属性

'use strict';

// 1-全局变量
var course = 'Learn JavaScript';

alert(course); // 'Learn JavaScript'
alert(window.course); // 'Learn JavaScript'

// 2-顶层函数
function foo() {
    alert('foo');
}

foo(); // 直接调用foo()
window.foo(); // 通过window.foo()调用

// JavaScript实际上只有一个全局作用域。
// 名字空间  创建一个对象,把所有的属性和方法放入其中,形成一个独有的全局变量
  1. 局部作用域:由于JavaScript的变量作用域实际上是函数内部,我们在for循环等语句块中是无法定义具有局部作用域的变量的:
// 3-局部变量
'use strict';
function foo() {
    for (var i=0; i<100; i++) {
        //
    }
    i += 100; // 仍然可以引用变量i
}

// 解决块级作用域,ES6引入 关键词:let 替代 var
function foo() {
    var sum = 0;
    for (let i=0; i<100; i++) {
        sum += i;
    }
    i += 1; // 报错 SyntaxError:
}

// 申明 常量变量名全部大写  --块级作用域

// 1-num
const PI = 3.14;

// 2-obj
const obj = {}
obj.name = 'allen'

console.log(obj.name) // 'allen'

// 3-obj
const obj = { name: 'allen' }
const _obj = obj
console.log(_obj) // { name: 'allen' }

obj.name = 'melo'
console.log(_obj) // { name: 'melo' }

// 备注:const所保证的是变量指向的内存地址不能够被改变,也就是保存的指针, 
// 并不是保证变量的值不可以被改变,同理,当我们操作数组的时候,也会出现类似的情况,
// 这就是引用传递(指针传递)

// 3-解构赋值
var [x, y, z] = ['hello', 'JavaScript', 'ES6'];
var {name, age, passport} = person;

二、箭头函数

语法

let fun=()=>{
  console.log('我是箭头函数')
};  // 箭头函数是一个匿名函数(anonymous function)

// 1-箭头函数是匿名函数,不能作为构造函数,不能使用new
let fc = new fun();  //  错误

// 2-箭头函数不绑定arguments,取而代之需要用展开运算符解决
let function c(a){
  console.log(arguments)
}
c(1,2,3,4,5)

let b=(...c)=>{
  console.log(arguments)
}
b(1,2,3,4,5) //uncaught ReferenceError: arguments is not defined

let c=(...c)=>{
  console.log(c)
};
c(1,2,3,4,5)

// 3-箭头函数不绑定this,会捕获其所在的上下文的this值,作为自己的this值;
let obj={
  a: 10;
  b: ()=>{
    console.log(this.a); // undefined
    console.log(this); //window{postmesage:f, blur: f, focus: f, close: f, frame: window, ...}
  }
  c: function(){
    console.log(this.a); // 10
    console.log(this); //{a:10, b:f, c:f}
  }
}
obj.b();
obj.c();

// 4-箭头函数通过 call() 或 apply() 方法调用一个函数时,只传入了一个参数,对 this 并没有影响;
// 5-箭头函数没有原型属性。

特点
1. 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象。
2. 不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。(展开运算符)
3. 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。
4. 不可以使用 new 命令,因为:

  • 没有自己的 this,无法调用 call,apply。
  • 没有 prototype 属性 ,而 new 命令在执行时需要将构造函数的 prototype 赋值给新的对象的 proto

三、构造函数

语法

// 实例对象 构造函数 构造函数的原型

function Person(){}  // 注意构造函数首字母大写
let person = new Person()
console.log(person); // 实例对象
console.log(Person); // 构造函数
console.log(person.__proto__); // 实例对象的原型
console.log(Person.prototype); // 构造函数的原型
console.log(Person.prototype.constructor); // 构造函数的原型的构造函数

console.log(person.__proto__ === Person.prototype) //true
console.log(Person.prototype.constructor===Person) //true
console.log(Object.getPrototypeOf(person) === Person.prototype) // true

// 1-创建原型
function Animal(){
  this.name=name;
  this.age=18; // 实例上的属性
}  // 构造函数

// 2-原型方法
Animal.prototype.address={
  location:"野外" // 公共属性
  console.log(this.name+location);
}; // 原型

// 3-实例化创建新的原型对象,新的原型对象会共享原型的属性和方法
let a1 = new Animal("猴子"); // 构造函数的调用(new关键词调用)
let a2 = new Animal("兔子"); // 实例对象

a1.adress() // 猴子野外
a2.adress() // 兔子野外

// 为新对象实例添加方法
// 通过原型创建的新对象实例是相互独立的
a1.food = function(){
  eat:"肉食"
}

a1.food(); // 肉食
a2.food();
// Uncaught TypeError: person2.getName is not a function

构造函数
1. 创建 --语法
2. 构造函数执行流程
1. 创建一个新的对象,这个对象的类型是 object;
2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象),可以使用this来引用新建的对象
3. 执行构造函数中的代码(为这个新对象添加属性)
4. 返回新对象

原型模式
1. 创建的每一个函数,解析器都会想函数中天机一个属性prototype,这个属性对应的一个对象,这个对象就是原型对象,唯一的,自己的。
2. 如果函数作为普通函数调用prototype没有任何作用。
3. 当函数以调用函数的形式调用时,他所创建的队形都会有一个隐含的属性,指向该构造函数的原型对象,我们可以通过**proto来访问该属性。
4. 原型对象就相当于一个公共的区域,所有
同一个类的实例**都可以访问到这个区域。
5. 将对象共有的属性和方法,统一添加到构造函数的原型对象中。
6. 原型对象也是对象,他也有原型。
7. 当我们访问对象的一个属性或方法时,它会先在对象自身中寻找,如果有则直接使用,如果没有则会去原型对象中寻找,如果原型对象中也没有,就去原型的原型中寻找,直到找到Object,Object对象的原型没有原型,如果还没,就是undefined

// 原型模式
function Person(name ,age ,gender){
	this.name = name;
	this.age = age;
	this.gender = gender;
}

Person.prototype.sayName = function(){
	alert(this.name);
}
var per = new Person("孙悟空",18,"男");
var per2 = new Person("猪八戒",18,"男");

console.log(per.sayName == per2.sayName);//true

原型链

  1. 定义:对象之间的继承关系,在JavaScript中是通过prototype对象指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条,专业术语称之为原型链。
  2. 用法:当我们访问对象的一个属性或方法时,它会先在对象自身中寻找,如果有则直接使用,如果没有则会去原型对象中寻找,如果找到则直接使用。如果没有则去原型的原型中寻找,直到找到Object对象的原型,Object对象的原型没有原型,如果在Object原型中依然没有找到,则返回null。每个实例都有__proto__ 指向所属类的原型

四、析构函数


五、匿名函数


// 普通函数通过函数名调用
function fn1 () {
  statements
}
fn1()

// 匿名函数通过引用调用
var fn2 = function () {
  statements
}
fn2() // 这里的fn2就是对匿名函数的引用

六、自执行函数


使用场景:隔离内外+立即执行,函数只会执行一次,之后就不用了。而非模块化和功能复用。

(function () {
    statements
})()

// 立即调用函数表达式(Immediately Invoked Function Expression, IIFE)
(function () {
    statements
}())

如何使用

  1. 惰性返回
    将 IIFE 分配给一个变量,不是存储 IIFE 本身,而是存储 IIFE 执行后返回的结果。

IIFE是惰性载入的,因为函数被执行引擎以同步的方式立即执行了,所以之后的代码访问这个变量时,可以直接返回计算后的结果。

// 使用匿名函数
var result = function () {
  var name = "Foo"
  return name
}
// 每次获取name, 匿名函数内部代码都将被执行一次
result() // "Foo"

// 使用IIEF
var result = (function () {
    var name = "Foo"
    return name
})();
// IIFE 执行后返回的结果,后续访问不需执行,类似“惰性”效果
result // "Foo"
  1. 模块化
    可以借助IIEF隔离作用域。避免污染,常用于广告、第三方统计等需求。
<!--示例: 百度统计-->
<script>
var _hmt = _hmt || [];
(function() {
  var hm = document.createElement("script");
  hm.src = "https://hm.baidu.com/hm.js?12345678223456783234567842345678";
  var s = document.getElementsByTagName("script")[0]; 
  s.parentNode.insertBefore(hm, s);
})();
</script>

JS里没有入口函数的概念,在现今的模块化方案成熟稳定之前,立即执行函数也常常用来充当主函数的角色。

// Vue.js v2.6.14 源码结构
(function (global, factory) {...}(this, function () {...}))

// jquery 3.6.0 源码结构
( function( global, factory ) {
    "use strict";
    if ( typeof module === "object" && typeof module.exports === "object" ) {...} else {...}
// Pass this if window is not defined yet
} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {...} );

七、This 指针


this是 JavaScript 语言的一个关键字。

它是函数运行时,在函数体内部自动生成的一个对象,只能在函数体内部使用。

var obj = {
  foo: function () { 
    console.log(this.bar) 
  },
  bar: 1
};
var foo = obj.foo;
var bar = 2;
obj.foo() // 1
foo() // 2

上述调用foo()函数,输出不同的结果,这种差异的原因,在于函数体内部使用了this关键字。this指的是函数运行时所在的环境

对于obj.foo()来说,foo运行在obj环境,所以this指向obj;对于foo()来说,foo运行在全局环境,所以this指向全局环境。所以,两者的运行结果不一样。

环境也叫执行上下文。
在这里插入图片描述

执行上下文的创建阶段,会生成变量对象,建立作用域链,确定this指向。

this的指向,是在函数被调用时确定的**。**也就是执行上下文被创建时确定的。

因此,一个函数中的this指向,可以非常灵活。比如下面的例子中,同一个函数由于调用方式的不同,this指向了不一样的对象。

var a = 10;
var obj = {
  a: 20
}

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

fn(); // 10
fn.call(obj); // 20

此外,在函数执行过程中,this一旦被确定,就不可更改。

var a = 10;
var obj = {
  a: 20
}

function fn() {
  // 这句话试图修改this,运行后会报错
  this = obj;
  console.log(this.a);
}

fn();

1. 内存的数据结构

JavaScript 语言之所以有this的设计,跟内存里面的数据结构有关系。

var obj = { foo:  5 };

上面的代码将一个对象赋值给变量obj。JavaScript 引擎会先在内存里面,生成一个对象{ foo: 5 },然后把这个对象的内存地址赋值给变量obj。

在这里插入图片描述

也就是说,变量obj是一个地址(reference)。后面如果要读取obj.foo,引擎先从obj拿到内存地址,然后再从该地址读出原始的对象,返回它的foo属性。

原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。举例来说,上面例子的foo属性,实际上是以下面的形式保存的。
在这里插入图片描述

{
  foo: {
    [[value]]: 5
    [[writable]]: true
    [[enumerable]]: true
    [[configurable]]: true
    [[get]]: undefined
    [[set]]: undefined
  }
}

注意,foo属性的值保存在属性描述对象的value属性里面。

属性值-函数
当对象属性的值为函数时,引擎会将函数单独保存在内存中,然后再将函数的地址赋值给foo属性的value属性。
var obj = { foo: function () {} };
在这里插入图片描述

{
  foo: {
    [[value]]: 函数的地址
    ...
  }
}

由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。

var f = function () {};
var obj = { f: f };

// 单独执行
f()

// obj 环境执行
obj.f()

2. 环境变量

JavaScript 允许在函数体内部,引用当前环境的其他变量。

var f = function () {
  console.log(x);
};

上面代码中,函数体里面使用了变量x。该变量由运行环境提供。

现在问题就来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this就出现了,它设计目的就是在函数体内部,指代函数当前的运行环境。

var f = function () {
  console.log(this.x);
}

上面代码中,函数体里面的this.x就是指当前运行环境的x。

var f = function () {
  console.log(this.x);
}

var x = 1;
var obj = {
  f: f,
  x: 2,
};

// 单独执行
f() // 1

// obj 环境执行
obj.f() // 2

上面代码中,函数f在全局环境执行,this.x指向全局环境的x。
在这里插入图片描述
在这里插入图片描述

obj.foo()是通过obj找到foo,所以就是在obj环境执行。
一旦var foo = obj.foo,变量foo就直接指向函数本身,所以foo()就变成在全局环境执行。

3. this 的绑定方式

默认绑定

默认绑定是在不使用其他绑定规则时的规则,通常是独立函数的调用。

function greeting() {
  console.log(`Hello, ${this.name}`);
}

var name = 'Eric';
greeting(); // Hello, Eric

隐式绑定

隐式绑定指的是在一个对象上调用函数。通过 obj 调用 greeting 方法,this 就指向了 obj。

将 obj.greeting 赋给了一个全局的变量 otherGreeting,所以在执行 otherGreeting 时,this 会指向 window。

function greeting() {
  console.log(`Hello,${this.name}`);
};

var name = 'Eric';

var obj = {
  name: 'World',
  greeting,
};

var otherGreeting = obj.greeting;

greeting(); // Hello,Eric
obj.greeting(); // Hello,World
otherGreeting().greeting(); 
// Hello,Eric

异步操作-隐式绑定丢失
如果涉及到回调函数(异步操作),就要小心隐式绑定的丢失问题。

function  greeting() {
  console.log(`Hello,${this.name}`);
};

var name = 'Eric';
var obj1 = {
  name: 'Obj1',
  greeting() {
    setTimeout(function() {
      console.log(`Hello,${this.name}`);
    })
  }
};

var obj2 = {
  name: 'Obj2',
  greeting,
};

obj1.greeting(); // Hello,Eric
obj2.greeting(); // Hello,Obj2
setTimeout(obj2.greeting, 100);
// Hello,Eric
setTimeout(function() {
  obj2.greeting();
}, 200); // Hello,Obj2

结果分析

obj1.greeting() 调用

因为涉及到异步操作setTimeout。在JavaScript中,一段代码执行时,会先执行宏任务中的同步代码,当遇到setTimeout之类的宏任务,那么就把这个 setTimeout 内部的函数推入「宏任务的队列」中,下一轮宏任务执行时调用。当本轮宏任务调用结束后,下一轮宏任务执行时,此时函数位于内存中,this指向全局环境。此时 this.name 就是 Eric。

obj2.greeting() 调用

greeting函数位于内存中,通过obj2来调用,那么this的指向环境变成了obj2。如前面讲述的图所示:
在这里插入图片描述

setTimeout(obj2.greeting, 100)调用

可以理解为将 obj2.greeting 赋值给一个新的变量(此时与obj1.greeting类似),所以此时 this 也是指向了 window。

setTimeout(function() {obj2.greeting();}, 200)调用

此时setTimeout的function参数里面包含了obj2.greeting()方法,调用则是隐式绑定,此时 this 指向 obj2。我们可以做个实验来验证

setTimeout(function() {
  console.log(this);
  obj2.greeting();
}, 200); 

其中,打印出来的this指向window,所以在200毫秒后,function回调函数里面的this环境指向window,这个与前面的实验obj1.greeting()调用是一致的,此时相当于在全局环境中,我们来调用了obj2.greeting(),这个与前面的实验obj2.greeting()调用是一致的,所以打印的结果是Hello,Obj2

显式绑定

显示绑定就是通过 call, apply, bind 来显式地指定 this 的绑定对象。

三者的第一个参数都是传递 this 指向的对象,call 与 apply 的区别是前者从第二个参数起传递一个参数序列,后者传递一个数组,call, apply 和 bind 的区别是前两个都会立即执行对应的函数,而 bind 方法不会。

我们通过 call 显式绑定 this 指向的对象来解决隐式绑定丢失的问题。

function  greeting() {
  console.log(`Hello,${this.name}`);
};

var name = 'Eric';

var obj = {
  name: 'Obj',
  greeting,
};

var otherGreeting = obj.greeting;

// 强制将 this 绑定到 obj
otherGreeting.call(obj); // Hello,Obj
setTimeout(obj.greeting.call(obj), 100); // Hello,Obj

在使用显式绑定时,如果将 null, undefined 作为第一个参数传入 call, apply 或者 bind,实际应用的是默认绑定。
function greeting() {
  console.log(`Hello,${this.name}`);
};

var name = 'Eric';

var obj = {
  name: 'Obj',
  greeting,
};

var otherGreeting = obj.greeting;
// this 仍然指向 window
otherGreeting.call(null); //Hello,Eric

绑定语法:

  • The call() method of Function instances calls this function with a given this value and arguments provided individually.

  • The apply() method of Function instances calls this function with a given this value, and arguments provided as an array (or an array-like object).

JavaScript内部提供了一种机制,让我们可以自行手动设置this的指向。它们就是call与apply。所有的函数都具有这两个方法。它们除了参数略有不同之外,其功能完全一样。它们的第一个参数都为this将要指向的对象。

二者都是函数对象Function的方法,且第一个参数都是要绑定对象的上下文。

call(thisArg)
call(thisArg, arg1)
call(thisArg, arg1, arg2)
call(thisArg, arg1, arg2, /* …, */ argN)

thisArg
The value to use as this when calling func. If the function is not in strict mode, null and undefined will be replaced with the global object, and primitive values will be converted to objects.

arg1, …, argN
Arguments for the function.

apply(thisArg)
apply(thisArg, argsArray)

thisArg
The value of this provided for the call to func. If the function is not in strict mode, null and undefined will be replaced with the global object, and primitive values will be converted to objects.

argsArray
An array-like object, specifying the arguments with which func should be called, or null or undefined if no arguments should be provided to the function.

如下例子所示。fn并非属于对象obj的方法,但是通过call,我们将fn内部的this绑定为obj,因此就可以使用this.a访问obj的a属性了。这就是call/apply的用法。

function fn() {
  console.log(this.a);
}
var obj = {
  a: 20
}

fn.call(obj);

call与applay后面的参数,都是向将要执行的函数传递参数。其中call以一个一个的形式传递,apply以数组的形式传递。这是他们唯一的不同。

function fn(num1, num2) {
  console.log(this.a + num1 + num2);
}
var obj = {
  a: 20
}

fn.call(obj, 100, 10); // 130
fn.apply(obj, [20, 10]); // 50

因为call/apply的存在,JavaScript变得更加灵活。也因此他们的使用场景就多种多样。简单总结几点,也欢迎大家补充。

将类数组对象转换为数组

function exam(a, b, c, d, e) {

  // 先看看函数的自带属性 arguments 什么是样子的
  console.log(arguments);

  // 使用call/apply将arguments转换为数组, 
  // 返回结果为数组,arguments自身不会改变
  var arg = [].slice.call(arguments);
  console.log(arg);
}

exam(2, 8, 9, 10, 3);

// result:
// { '0': 2, '1': 8, '2': 9, '3': 10, '4': 3 }
// [ 2, 8, 9, 10, 3 ]
//
// 也常常使用该方法将DOM中的nodelist转换为数组
// [].slice.call( document.getElementsByTagName('li') );

// 将类数组对象转化为数组的方法
const args = Array.prototype.slice.call(arguments);
// or
const args = [].slice.call(arguments)
// or
const args = Array.from(arguments);
// or
const args = [...arguments];

根据自己的需要灵活修改this指向

var foo = {
  name: 'joker',
  showName: function () {
    console.log(this.name);
  }
}
var bar = {
  name: 'rose'
}
foo.showName.call(bar);

实现继承

// 定义父级的构造函数
var Person = function (name, age) {
  this.name = name;
  this.age = age;
  this.gender = ['man', 'woman'];
}

// 定义子类的构造函数
var Student = function (name, age, high) {
  // use call
  Person.call(this, name, age);
  this.high = high;
}
Student.prototype.message = function () {
  console.log('name:' + this.name + ', 
  age:' + this.age + ', 
  high:' + this.high + ', 
  gender:' + this.gender[0] + ';');
}

new Student('xiaom', 12, '150cm').message();

// result
// ----------
// name:xiaom, age:12, high:150cm, gender:man;

在Student的构造函数中,借助call方法,将父级的构造函数执行了一次,相当于将Person中的代码,在Sudent中复制了一份,其中的this指向为从Student中new出来的实例对象。

call方法保证了this的指向正确,因此就相当于实现了继承。Student的构造函数等同于下。

var Student = function (name, age, high) {
  this.name = name;
  this.age = age;
  this.gender = ['man', 'woman'];
  // Person.call(this, name, age); 这一句话,相当于上面三句话,因此实现了继承
  this.high = high;
}

在向其他执行上下文的传递中,确保this的指向保持不变

如下面的例子中,我们期待的是getA被obj调用时,this指向obj,但是由于匿名函数的存在导致了this指向的丢失,在这个匿名函数中this指向了全局,因此我们需要想一些办法找回正确的this指向。

var obj = {
  a: 20,
  getA: function () {
    setTimeout(function () {
      console.log(this.a)
    }, 1000)
  }
}

obj.getA();

常规的解决办法很简单,就是使用一个变量,将this的引用保存起来。我们常常会用到这方法,但是我们也要借助上面讲到过的知识,来判断this是否在传递中被修改了,如果没有被修改,就没有必要这样使用了。

var obj = {
  a: 20,
  getA: function () {
    var self = this;
    setTimeout(function () {
      console.log(self.a)
    }, 1000)
  }
}

另外就是借助闭包与apply方法,封装一个bind方法。

function bind(fn, obj) {
  return function () {
    return fn.apply(obj, arguments);
  }
}

var obj = {
  a: 20,
  getA: function () {
    setTimeout(bind(function () {
      console.log(this.a)
    }, this), 1000)
  }
}

obj.getA();

当然,也可以使用ES5中已经自带的bind方法。它与我上面封装的bind方法是一样的效果。

var obj = {
  a: 20,
  getA: function () {
    setTimeout(function () {
      console.log(this.a)
    }.bind(this), 1000)
  }
}

ES6中也常常使用箭头函数的方式来替代这种方案

4. this 的使用

全局对象中的 this

关于全局对象的this,我之前在总结变量对象的时候提到过,它是一个比较特殊的存在。全局环境中的this,指向它本身。因此,这也相对简单,没有那么多复杂的情况需要考虑。

// 通过this绑定到全局对象
this.a2 = 20;

// 通过声明绑定到变量对象,但在全局环境中,变量对象就是它自身
var a1 = 10;

// 仅仅只有赋值操作,标识符会隐式绑定到全局对象
a3 = 30;

// 输出结果会全部符合预期
console.log(a1);
console.log(a2);
console.log(a3);

全局下的 this

  • 非严格模式情况下,全局的 this 都是 window。
console.log( this );  // window
function fn(){
    console.log( this );  // window
}
  • 严格模式下,抑制 this。

禁用指向了 window 的 this,让 this 指向 undefined。

"use strict";
function foo(){
   console.log(this);  // undefined
}
foo()

函数中的 this

在总结函数中this指向之前,我想我们有必要通过一些奇怪的例子,来感受一下函数中this的捉摸不定。

// demo01
var a = 20;
function fn() {
  console.log(this.a);
}
fn();
 
// demo02
var a = 20;
function fn() {
  function foo() {
    console.log(this.a);
  }
  foo();
}
fn();
 
// demo03
var a = 20;
var obj = {
  a: 10,
  c: this.a + 20,
  fn: function () {
    return this.a;
  }
}

console.log(obj.c);
console.log(obj.fn());

这几个例子需要花点时间仔细感受一下,如果你暂时没想明白怎么回事,也不用着急,我们一点一点来分析。

分析之前,我们直接了当抛出结论。

在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。如果函数独立调用,那么该函数内部的this,则指向undefined。但是在非严格模式中,当this指向undefined时,它会被自动指向全局对象。

从结论中我们可以看出,想要准确确定this指向,找到函数的调用者以及区分他是否是独立调用十分关键。

// 为了能够准确判断,我们在函数内部使用严格模式,因为非严格模式会自动指向全局
function fn() {
  'use strict';
  console.log(this);
}

fn();  // fn是调用者,独立调用
window.fn();  // fn是调用者,被window所拥有

在上面的简单例子中,fn()作为独立调用者,按照定义的理解,它内部的this指向就为undefined。而window.fn()则因为fn被window所拥有,内部的this就指向了window对象。

掌握了这个规则,现在回过头去看看上面的三个例子,通过添加/去除严格模式,你就会发现,原来this已经变得不那么虚无缥缈,已经有迹可循了。

但是我们需要特别注意的是demo03。在demo03中,对象obj中的c属性使用this.a + 20来计算。这里我们需要明确的一点是,单独的{}不会形成新的作用域,因此这里的this.a,由于并没有作用域的限制,它仍然处于全局作用域之中。所以这里的this其实是指向的window对象。

那么我们修改一下demo03的代码,大家可以思考一下会发生什么变化。

'use strict';
var a = 20;
function foo() {
  var a = 1;
  var obj = {
    a: 10,
    c: this.a + 20,
    fn: function () {
      return this.a;
    }
  }
  return obj.c;
}
console.log(foo());    // ?
console.log(window.foo());  // ?

实际开发中,并不推荐这样使用this;

上面多次提到的严格模式,需要大家认真对待,因为在实际开发中,现在基本已经全部采用严格模式了,而最新的ES6,也是默认支持严格模式。

再来看一些容易理解错误的例子,加深一下对调用者与是否独立运行的理解。

var a = 20;
var foo = {
  a: 10,
  getA: function () {
    return this.a;
  }
}
console.log(foo.getA()); // 10

var test = foo.getA;
console.log(test());  // 20

foo.getA()中,getA是调用者,他不是独立调用,被对象foo所拥有,因此它的this指向了foo。而test()作为调用者,尽管他与foo.getA的引用相同,但是它是独立调用的,因此this指向undefined,在非严格模式,自动转向全局window。

稍微修改一下代码,大家自行理解。

var a = 20;
function getA() {
  return this.a;
}
var foo = {
  a: 10,
  getA: getA
}
console.log(foo.getA());  // 10
 
灵机一动,再来一个。如下例子。
function foo() {
  console.log(this.a)
}

function active(fn) {
  fn(); // 真实调用者,为独立调用
}

var a = 20;
var obj = {
  a: 10,
  getA: foo
}

active(obj.getA);

构造函数与原型方法上的 this

在封装对象的时候,我们几乎都会用到this,但是,只有少数人搞明白了在这个过程中的this指向,就算我们理解了原型,也不一定理解到了this。所以这一部分,我认为将会为这篇文章最重要最核心的部分。理解了这里,将会对你学习JS面向对象产生巨大的帮助。

结合下面的例子,我抛出几个问题大家思考一下。

function Person(name, age) {

    // 这里的this指向了谁?
    this.name = name;
    this.age = age;   
}

Person.prototype.getName = function() {

    // 这里的this又指向了谁?
    return this.name;
}

// 上面的2个this,是同一个吗,他们是否指向了原型对象?

var p1 = new Person('Nick', 20);
p1.getName();

我们已经知道,this,是在函数调用过程中确定,因此,搞明白new的过程中到底发生了什么就变得十分重要。

通过new操作符调用构造函数,会经历以下4个阶段。

  1. 创建一个新的对象;
  2. 将构造函数的this指向这个新对象;
  3. 指向构造函数的代码,为这个对象添加属性,方法等;
  4. 返回新对象。

因此,当new操作符调用构造函数时,this其实指向的是这个新创建的对象,最后又将新的对象返回出来,被实例对象p1接收。因此,我们可以说,这个时候,构造函数的this,指向了新的实例对象:p1。

而原型方法上的this就好理解多了,根据上边对函数中this的定义,p1.getName()中的getName为调用者,他被p1所拥有,因此getName中的this,也是指向了p1。

回调函数的 this

事件的 this 指向比较特殊: this => 当前发生事件的元素
其余所有的情况 this 指向都指向 window

function ab(fn) {
    fn(); } function cd() {
    // this因为回调函数重定向指向window
    console.log(this);  // window }

ab(cd);

var obj = {
    a: function(fn){
        fn();
    },
    b: function(){
        // 如果直接执行obj.b() 这里的this应该是obj对象
        // 如果通过上面a方法回调执行当前b的时候,this被重定向到window
        console.log(this);  // window
    } }

obj.a(obj.b);

// forEach, filter, reduce, some, every, flatMap  // 等里面都是回调函数都指向 window var obj = {
    a: function () {
        setTimeout(function () {
            console.log(this)  // window,匿名回调函数
        }, 100);
        // 所有的函数一旦被写在一个方法中,这个函数就是匿名的回调函数,
        // 在该函数中this指向window
        setInterval(function(){
            console.log(this);  // window
        },100)
        var arr=[1,2,3];
        arr.map(function(){
            console.log(this);  // window
            // 匿名回调函数
        });
        new Promise(function(resolve,reject){
            console.log(this);  // window
        })
    } }

对象中的 this

属性描述 this 时,这个时候对象还没有生成,所以 this 指向外层的 this 指向;

对象的方法中 this 是该对象本身。

var a = 10;
var obj1 = {
    a: 100,
    // window,对象当中,this指向window,
    // 因为是属性描述,对象还没有生成,所以指向外层this指向
    c: this.a,
    // 对象的属性函数中,this指向对象obj1
    init: function () {
        var obj = {
            a: 1,
            c: this.a,  // 属性描述this,obj1
            b: function () {
                // obj对象的方法中this指向obj对象
                console.log(this.a);  // 1
            }
        }
    }
}  
// 到这里对象算是创建完成

var a = 20;
var obj = {
    a: 1
}
obj.b = {
    a: 100,
    c: this.a,  // 20
    // 创建obj.b.c的时候,obj.b还没创建完成,所以不认为b是obj上的属性,
    // 所以this依然指向外层this的指向,也就就是window
    d: function() {
        console.log( this.a );  // 100 this指向本属性
    }
}

console.log( obj.b.c );  // 20

var obj = {
    a: 1,
    b: function() {
        // this -> obj
        var o = {
            // 判定o对象没有创建完,this指当前对象外this的指向,
            // 这里的this指向是obj
            c: this.a  // 1
        }
        console.log( o.c );  // 1
    }
}

obj.b();

事件 this 指向

事件侦听的回调函数是特殊的回调函数,this 是被重新处理了,this 指向被侦听的对象

document.addEventListener("click", clickHandler);

function clickHandler(e) {
    // this === e.currentTarget
}

var obj = {
    a: function(){
        // 点击后触发,打印document
        document.addEventListener("click", this.b);  
        this.b();  // 直接运行 打印obj对象
    },
    b: function(e){
        // 因为b函数是事件侦听的回调函数,
        // 因此这里this指向事件侦听的对象,也就是document
        console.log(this);
    }
}
obj.a();

类中的 this 指向

类中的 this 基本都是指向实例化后的对象,静态的 this 指向类本身

构造函数内部 this 指向实例对象

class Box{
    // ES7后才有的
    // 这里的内容是constructor执行后赋值
    b = 3;
    constructor() {
        this.a = this.b;
        Box.b = 20;
        Box.a = Box.b;
        // 之前没有static所以都是上述方式,来静态操作,上下等同
        // static b = 20; 
        // 一旦被设为静态属性,this指向都会被设置为当前的类名Box
        // static a = this.b;  
    } 
    play(){
        // 这里的this都是实例化的对象
        console.log(this.a);
    }

    static run(){
       console.log(this);
        // this
        // 因为使用static定义的方法,this指向为当前类名Box
        // 对于面向对象语言来说,一般在静态属性和方法中不允许使用this这个概念
    }    
}

// 新建类然后继承
class Ball extends Box{
    constructor() {
        super();
    }
}

var b = new Ball();
Ball.run();  // 这里打印的Ball类,继承会重定向this到新类中

// 继承后静态方法也会被继承
var b = new Box();
console.log( b.a );  // 3
b.play();  // 3
console.log( Box.a );  // 20

Box.run()  // 打印box类

箭头函数的 this 指向

所有箭头函数内的 this 都是指当前函数外的 this 指向

var fn = () => {

};
var obj = {
    a: () => {
        console.log(this);  // this --> window
    },
    b: function() {
        console.log(this);  // this --> obj
    },
    c() {
        console.log(this);  // this --> obj
    }
}
obj.a();  // window
obj.b();  // obj
obj.c();  // obj

var obj = {
    a: function() {
        setTimeout(() => {
            // 因为本来是回调函数,this统一都会被执行window
            // 但是使用了箭头函数,就会全部把这里
            // this指向setTimeout外的this指向,也就是obj
            console.log(this);  // obj
        }, 100)
    }
}
obj.a();  // obj

var obj = {
  hi: function() {
    console.log(this);
    return () => {
      console.log(this);
    };
  },

  sayHi: function() {
    return function() {
      console.log(this);
      return () => {
        console.log(this);
      };
    };
  },

  say: () => {
    console.log(this);
  }
};

let hi = obj.hi(); // 输出 obj 对象
hi(); // 输出 obj 对象
let sayHi = obj.sayHi();  // 输出 window
fun1(); // 输出 window
obj.say(); // 输出 window

结果分析:

  1. 第一步是隐式绑定,此时 this 指向 obj,所以打印出 obj 对象
  2. 第二步执行 hi()方法,虽然看着像闭包,但这是一个箭头函数,它会继承上一层的 this,也就是 obj,所以打印出 obj 对象
  3. 因为obj.sayHi() 返回一个闭包,所以 this 指向 window,因此打印出 window 对象 同样箭头函数继承上一层的
    this,所以 this 指向 window,因此打印出 window 对象
  4. 最后一次输出,因为 obj 中不存在this,因此按作用域链找到全局的 this,也就是 window,所以打印出 window 对象
var obj = {
  name: 'Eric',
  greeting() {
    setTimeout(() => {
      console.log(`Hello, ${this.name}`);
    })
  },
  greeting2() {
    console.log(`Hello, ${this.name}`);
  },

  greeting3() {
    setTimeout(function() {
      console.log(`Hello, ${this.name}`);
    });
  }
};

var name = 'Global';
obj.greeting();   //Hello, Eric
obj.greeting2();  //Hello, Eric
obj.greeting3();  //Hello, Global

结果分析:

  1. obj.greeting(),虽然 setTimeout 会将 this 指向全局,但箭头函数继承上一层的 this,也就是obj.greeting() 的 this,因为这是一个隐式绑定,所以 this 指向 obj,所以箭头函数的 this 也会指向
  2. obj. obj.greeting2(),这里是一个隐式绑定,所以 this 指向
  3. obj greeting3(),setTimeout会将 this 指向全局

ES5 中的 this 指向

因为 ES5 中没有类的方法,所以我们都是如下方式建立一个类

function Box() {  // 构造函数
    this.play();
    // 构造函数中this,new出的对象
}
Box.a = function() {
    // 这个方法类似于ES6的静态方法
    // this --> Box
}
Box.b = 3;  // 和ES6静态定义属性一样
Box.prototype.play = function() {
    // this 就是当前调用该方法的实例对象
}
Box.prototype.c = 10;

console.log(Box.b);  // 3
Box.a();

var b = new Box();
b.play()
// b对象没有play这个对象属性,因此就去原型链中找到最近的play方法,
// 执行这个play方法是由b这个对象执行
console.dir(Box);  
// 打印Box类, console.dir()可以显示一个对象的所有属性和方法
  • 因为 box 只是一个函数所以用 console.dir() 打印成为一个类
  • 静态方法只和类有关系,与实例化的对象没有任何关系

实战练习

案例一

var number = 5;
var obj = {
  number: 3,
  fn: (function() {
    var number;
    this.number *= 2;
    number = number * 2;
    number = 3;
    return function() {
      var num = this.number;
      this.number *= 2;
      console.log(num);
      number *= 3;
      console.log(number);
    };
  })(),
};
var fn = obj.fn;
fn.call(null);
obj.fn();
console.log(window.number);

因为 obj.fn 是一个立即执行函数(this 会指向 window),所以在 obj 创建时就会执行一次,并返回闭包函数。

var number; // 创建了一个私有变量 number 但未赋初值
this.number *= 2; // this.number 指向的是全局那个 number,所以 window.number = 10
number = number * 2; // 因为私有变量 number 未赋初值,所以乘以 2 会变为 NaN
number = 3; // 此时私有变量 number 变为 3

接着执行下面两句:

var fn = obj.fn;
fn.call(null);

因为将 obj.fn 赋值给一个全局变量 fn,所以此时 this 指向 window。接着,当 call 的第一个参数是 null 或者 undefined 时,调用的是默认绑定,因此 this 仍然指向 window.

var num = this.number; // 因为 window.number = 10,所以 num 也就是 10
this.number *= 2; // window.number 变成了 20
console.log(num); // 打印出 10
number *= 3; // 因为是闭包函数,有权访问父函数的私有变量,所以此时 number 为 9
console.log(number); // 打印出 9

当执行 obj.fn(); 时,此时的 this 指向的是 obj:

var num = this.number; // 因为 obj.number = 3,所以 num 也就为 3
this.number *= 2; // obj.number 变为 6
console.log(num); // 打印出 3
number *= 3; // 上一轮私有变量为变成了 9,所以这里变成 27
console.log(number); // 打印出 27

最后打印出 window.number 就是 20

最终结果:

10
9
3
27
20

案例二

var length = 10;

function fn() {
  console.log(this.length);
}

var obj = {
  length: 5,
  method: function(fn) {
    fn();
    arguments[0]();
  },
};

obj.method(fn, 1);

最终结果:

10
2

传入了 fn 而非 fn(),相当于把 fn 函数赋值给 method 里的 fn 执行,所以这里是默认绑定,此时 this 指向 window,所以执行 fn() 时会打印出 10。

arguments[0],就相当于执行 fn(),所以是隐式绑定,此时 this 指向 arguments,所以 this.length 就相当于 arguments.length,因为我们传递了两个参数,因此返回 2。

window.val = 1;

var obj = {
  val: 2,
  dbl: function() {
    this.val *= 2;
    val *= 2;
    console.log('val:', val);
    console.log('this.val:', this.val);
  },
};

obj.dbl();
var func = obj.dbl;
func();

最终结果:

2, 4
8, 8
  • 第一次调用是隐式调用,因此 this 指向 obj,所以 this.val 也就是 obj.val 变成了 4,但是 dbl方法中没有定义 val,所以会沿着作用域链找到 window.val,所以会依次打印出 2,4
  • 第二次是默认调用,this 指向window,window.val 会经历两次乘 2 变成 8,所以会依次打印出 8,8

5. this 总结

  1. 函数是否在 new 中调用(new 绑定),如果是,那么 this 绑定的是新创建的对象。
  2. 函数是否通过 call,apply调用,或者使用了 bind(即硬绑定),如果是,那么 this 绑定的就是指定的对象。
  3. 函数是否在某个上下文对象中调用(隐式绑定),如果是的话,this 绑定的是那个上下文对象。一般是 obj.foo()
  4. 如果以上都不是,那么使用默认绑定。
  5. 如果在严格模式下,则绑定到 undefined,否则绑定到全局对象。
  6. 如果把 Null 或者undefined 作为 this 的绑定对象传入 call、apply 或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
  7. 如果是箭头函数,箭头函数的 this 继承的是外层代码块的 this。
  • 10
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值