前端基础培训之 - this、闭包、原型

142 阅读5分钟

this是什么

传统oop语言里,this表示当前对象、或者当前对象一个实例,通过this可以获得当前对象属性和方法

JavaScript中的this略有不同,各书中的解释:

1、ECMAScript规范:this关键字执行为当前执行环境的 ThisBinding。
2、JavaScript高程:this对象是在运行时基于函数的执行环境绑定的。
3、你不知道的JavaScript:this是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用

大致的意思都是一样,就是this的指向会随着执行环境而改变。煮个通俗点的栗子,小时候跟同学说来我家玩,这个家指的是你和父母一起的家,当你长大成家后这个家就是你和妻子新建立的家。

如何判断运行中函数的this绑定

1、由new调用
function Person(name) {
    this.name = name;
    this.age = 100;
    this.say = function () {
        console.log(this.name + ' : ' + this.age);
    }
}

let person = new Person('奥特曼');
person.name; // 奥特曼
person.say(); // 奥特曼 : 100 

2、call、apply、bind
let person = {
    name: '奥特曼',
    age: 100
};
function say(word) {
    console.log(this.name + ' : ' + this.age + ' ' + word)
}
say.call(person, 'hello'); // 奥特曼 : 100 hello
// say.apply(person, ['hello']);
// let fn = say.bind(person); fn();

3、上下文对象调用
let person = {
    name: '奥特曼',
    getName: function () {
        return this.name;
    }
};
person.getName(); // 奥特曼
let getName = person.getName;
getName(); // undefined
4、默认
function foo(){
    return this; // window
}
console.log(foo() === window); // true

function foo() {
    "use strict";
    console.log(this); // undefined
}
5、箭头函数
function foo() {  
    setTimeout(() => {
        console.log(this.a);
    },100)
}
var obj = {
  a: 2
}
foo.call(obj);

this小结

  • 1、由new调用?绑定到新创建的对象
  • 2、用call或apply或bind调用?绑定到指定的对象
  • 3、用上下文对象调用?绑定到那个上下文对象
  • 4、默认:在严格模式下绑定undefined,否则绑定到全局对象

注意:箭头函数不使用上面规则他会根据当前的词法作用域来决定this,具体来说箭头函数会继承外层函数调用的this绑定,又或者说没有自己的this都指向外层

练练手

1、模拟实现apply、call、bind

作用:用来修改函数中this的指向, 例如fn.call(obj) 是将fn中的this指向obj对象
实现:根据小结内容可以根据第3点用上下文对象调用来模拟实现

Function.prototype.myCall = function () {
    let [thisArgs, ...args] = [...arguments]; // arguments 结构出对象与参数
    thisArgs = Object(thisArgs); // 将对象包装一下
    let fn = Symbol(); // symbol 获取唯一值,防止对象下的key重名
    thisArgs[fn] = this;   //  this做为调用的函数,将其挂载到要绑定的对象下
    let result = thisArgs[fn](...args); // 执行函数并返回值
    delete thisArgs[fn];  // 删除之前对象下添加的函数
    return result;  // 返回值
}

let person = {
    name: '奥特曼',
    age: 100
};
function say(word) {
    console.log(this.name + ' : ' + this.age + ' ' + word)
}
say.myCall(person, 'hello'); // 奥特曼 : 100 hello
// apply 实现同理
// bind 只是绑定this,并不会立即执行,所以只需在call外面包装一层
// 箭头函数中没有arguments, 可以用拓展运算符

Function.prototype.myBind = function () {
    let [thisArgs, ...args] = [...arguments];
    return (...newArgs) => {
        return this.call(thisArgs, ...args, ...newArgs);    
    };
}

let person = {
    name: '奥特曼',
    age: 100
};
function say(...item) {
    console.log(this.name + ' : ' + this.age, item);
}
let fn = say.myBind(person, 'hello'); 
fn('world');    

2、模拟实现new

使用new来调用函数的大致过程:

  • 1.创建(或者说构造)一个全新的对象。
  • 2.这个新对象会被执行[[Prototype]]连接。
  • 3.将这个对象作为构造函数的 this
  • 4.如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function myNew(fn, ...args) {
    let obj = Object.create(fn.prototype);
    let result = fn.call(obj, ...args);
    return typeof result === 'object' ? result : obj;
}

闭包

闭包是什么?

MDN的解释:闭包是函数和声明该函数的词法环境的组合

javascript高程:一个有权访问另一个函数作用域中变量的函数

你不知道的JavaScript:当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包

闭包的作用

闭包最大的作用就是隐藏变量,闭包的一大特性就是内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回(寿命终结)了之后, 基于此特性,JavaScript可以实现私有变量、特权变量、储存变量等

function Person() {
    var name = '奥特曼';
    this.getName = function() {
        return name;
    }
    this.setName = function(value) {
        name = value;
    }
}

const cxk = new Person()

console.log(cxk.getName()) // 奥特曼
cxk.setName('泰罗')
console.log(cxk.getName()) // 泰罗
console.log(name) // name is not defined

原型

给其它对象提供共享属性的对象,所以原型也是对象。

1、原型对象是什么

绝大部分的函数(少数内建函数除外)都有一个prototype属性,这个属性是原型对象用来创建新对象实例,而所有被创建的对象都会共享原型对象,因此这些对象便可以访问原型对象的属性。

2、原型链是如何形成的

原因是每个对象都有 __ proto__ 属性,此属性指向该对象的构造函数的原型。

对象可以通过 __ proto__与上游的构造函数的原型对象连接起来,而上游的原型对象也有一个__ proto__,这样就形成了原型链。

3、继承

function Parent(name) {
    this.parent = name
}
Parent.prototype.say = function() {
    console.log(`${this.parent}: 你好帅`)
}
function Child(name, parent) {
    // 将父类的构造函数绑定在子类上
    Parent.call(this, parent)
    this.child = name
}

/** 
 1. 这一步不用Child.prototype =Parent.prototype的原因是怕共享内存,修改父类原型对象就会影响子类
 2. 不用Child.prototype = new Parent()的原因是会调用2次父类的构造方法(另一次是call),会存在一份多余的父类实例属性
3. Object.create是创建了父类原型的副本,与父类原型完全隔离
*/
Child.prototype = Object.create(Parent.prototype);
Child.prototype.say = function() {
    console.log(`${this.parent}好,我是${this.child}`);
}

// 注意记得把子类的构造指向子类本身
Child.prototype.constructor = Child;

var parent = new Parent('father');
parent.say() // father: 你好帅

var child = new Child('奥特曼', 'father');
child.say() // father好,我是奥特曼

练手

1、模拟实现instanceOf

function instance_of(L, R) {
  //L 表示左表达式,R 表示右表达式
  var O = R.prototype; // 取 R 的显示原型
  L = L.__proto__; // 取 L 的隐式原型
  while (true) {
    if (L === null) return false;
    if (O === L)
      // 这里重点:当 O 严格等于 L 时,返回 true
      return true;
    L = L.__proto__;
  }
}