this、原型和原型链、作用域和作用域链

涉及到JavaScript高级的知识,永远都躲不过this、原型、原型链、作用域和作用域链。但是拗口的概念又经常让人描述得不准确,在此做个记录。

1、this

代表函数调用相关联的对象,通常也称之为执行上下文

1.1、作为函数直接调用,非严格模式下,this指向window,严格模式下,this指向undefined

// 非严格模式
function foo() {
    console.log(this)
}
foo()  // window

// 严格模式
"use strict"
function foo() {
    console.log(this)
}
foo()  // undefined

1.2、作为某对象的方法调用,this通常指向调用的对象。

let foo = {
  bar: function() { 
    console.log(this) 
  }
}

foo.bar()   // foo

1.3.、使用apply、call、bind 可以绑定this的指向(不管给函数 bind 几次,函数中的this 永远由第一次 bind 决定)。

let a = {}
let fn = function () { console.log(this) }
fn.bind(a)()  // a

let a = {}
let fn = function () { console.log(this) }

fn.bind().bind(a)()  // window

1.4、 在构造函数中,this指向新创建的对象

function Foo() {
    this.name = 'hi';
    console.log(this)
}
new Foo()  // Foo {name: "hi"}

1.5、 箭头函数没有单独的this值,this在箭头函数创建时确定,它与声明所在的上下文相同。

let a = {
  b: function() { 
    console.log(this) 
  },
  c: () => {
    console.log(this)
  }
}

a.b()   // a
a.c()   // window

this判断 下面输出为多少?

var name1 = 1;

function test() {
    let name1 = 'kin';
    let a = {
        name1: 'jack',
        fn: () => {
      var name1 = 'black'
      console.log(this.name1)
    }
  }
    return a;
}

test().fn() // ?

答案: 输出1
因为fn处绑定的是箭头函数,箭头函数并不创建this,它只会从自己的作用域链的上一层继承this。这里它的上一层是test(),非严格模式下testthis值为window

  • 如果在绑定fn的时候使用了function,那么答案会是 jack
  • 如果第一行的 var 改为了 let,那么答案会是 undefind, 因为let不会挂到window

再来一题:

var num = 1;
var myObject = {
  num: 2,
  add: function() {
    this.num = 3;
    (function() {
      console.log("第1个出现的console:" + this.num);
      this.num = 4;
    })();
    console.log("第2个出现的console:" + this.num);
  },
  sub: function() {
    console.log("第3个出现的console:" + this.num);
  }
};

myObject.add();
console.log("第4个出现的console:" + myObject.num);
console.log("第5个出现的console:" + num);
var sub = myObject.sub;
sub();

下面来看正确答案:

第1个出现的console:1
第2个出现的console:3
第4个出现的console:3
第5个出现的console:4
第3个出现的console:4

1.2、作为某对象的方法调用,this通常指向调用的对象。

  • myObject.add()里,第1个出现的console在立即执行函数里,根据上面提到过的,那么这个1.2所说,立即执行函数在myObject.add()里,this应该指向myObject。然而,立即执行函数中的this指向window,因为立即执行函数是window调用的,所以,第1个出现的console的值为1。

  • 第1个出现的console执行完以后,this.num = 4,改变的是window中的值。所以第5个出现的console的值为4。

  • var sub = myObject.sub;,此时sub的环境也是window,所以第3个出现的console的值也是4。

一个小小的吐槽 ~ 之前以为自己把 this搞懂了,一个立即执行函数 IEFF让我遭遇了 this滑铁卢。

多个this规则出现时,this最终指向哪里?

首先,new 的方式优先级最高,接下来是 bind 这些函数,然后是 obj.foo() 这种调用方式,最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。

2、原型与原型链

2.1、原型对象

每一个JavaScript对象(null除外)都和另一个对象相关联,“另一个”对象就是我们熟知的原型,每一个对象都是从原型继承属性

每一个函数都有一个prototype(原型)属性,这个属性指向函数的原型对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

  • 《JavaScript高级程序设计》(第3版) 中提到:prototype就是通过调用构造函数而创建的那个对象实例的原型对象。
  • 《JavaScript权威指南》(第6版) 中也提到:通过new关键字和构造函数调用创建的对象的原型就是构造函数的prototype属性的值。
function Person(){};
Person.prototype.name = "wood";
Person.prototype.job = "engineer";
Person.prototype.sayName = function(){
    console.log(this.name);
}

var person1 = new Person();
person1.sayName();  // wood

var person2 = new Person();
person2.sayName();  // wood

console.log(person1.sayName == person2.sayName);  // true

所以通过上述例子可知:Person.prototype就是实例person1和实例person2的原型对象。


在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这是一个指向prototype属性所在函数的指针。

也就是说每个原型都有都有一个 constructor 属性,指向了原型所在的函数,在上面例子来说,Person.prototype.constructor == Person

上述代码中各个对象之间的关系

有细心的小伙伴会注意到,为什么实例person1person2之中是[[prototype]]
—— 当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262第5版中管这个指针叫[[prototype]]。虽然在脚本中没有标准的方式访问[[prototype]],但Firfox、Safari和Chrome在每个对象上都支持一个__proto__的属性(来自-《JavaScript高级程序设计》(第三版))。
所以在有的地方会直接说,实例person1person2__proto__属性,指向Person.prototype

注意:我们无法直接访问到[[prototype]]__proto__,可以通过 isPrototypeOf()方法判断某个原型和对象实例是否存在关系,或者,也可以使用 Object.getPrototypeOf() 获取一个对象实例 __proto__ 属性的值。

console.log(Person.prototype.isPrototypeOf(person1)); // true
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true

更简单的原型语法
前面例子中,每添加一个属性和方法就要敲一次Person.prototype,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象,如下所示:

function Person(){};

Person.prototype = {
    name : "wood",
    job: "engineer",
    sayName : function () {
        console.log(this.name);
    }
};

重写后的代码与原代码最终结果相同。但是有一个例外:constructor属性不再指向Person了。我们在前面提到过,每创建一个函数,就会同时创建它的prototype对象,这个对象也自动获得constructor属性。而我们这样重写,本质上完全重写了默认的prototype对象,因此constructor属性也就变成了新对象的constructor属性,不再指向Person函数了。此时,尽管instanceof操作符还能返回正确结果,但是通过constructor已经无法确定对象的类型了,结果如下所示:

var friend = new Person();

console.log(friend instanceof Object);  // true
console.log(friend instanceof Person);  // true
console.log(friend.constructor == Person);  // false
console.log(friend.constructor == Object);  // true

从上可见,constructor属性等于Object而不是等于Person了。如果constructor的值真的很重要,可以像下面这样特意将它设置回适当的值:

function Person(){};

Person.prototype = {
    constructor : Person,
    name : "wood",
    job: "engineer",
    sayName : function () {
        console.log(this.name);
    }
};

以上代码特意包含了一个constructor属性,并将它的值设置为Person,从而确保了通过该属性能够访问到适当的值。
但是,重设constructor属性后会导致它的[[Enumerable]]特性被设置为true。默认情况下,原生的constructor属性是不可枚举的。因此,可通过Object.defineProperty()来重设构造函数,如下代码所示:

function Person(){};

Person.prototype = {
    name : "wood",
    job: "engineer",
    sayName : function () {
        console.log(this.name);
    }
};

Object.defineProperty(Person.prototype, "constructor", {
    enumerable: false;
    value: Person
})

2.2、原型链

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

而每一个原型对象都是个普通对象,普通对象都具有原型。所有的内置构造函数以及大部分自定义的构造函数都具有一个继承自Object.prototype的原型(所有函数的默认原型都是Object的实例)。例如(关系示意见下图),Date.prototype的属性继承自Object.prototype,因此由new Date()创建的Date对象的属性同时继承自Date.prototypeObject.prototype。这一系列链接的原型对象就是所谓的“原型链”。

原型链关系示意图

当以读取模式访问一个实例属性时,首先会在实例中搜索该属性。如果没有找到该属性,则会继续搜索实例的原型。在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。

可以用instanceof操作符和isPrototypeOf()方法来确定原型和实例的关系。

谨慎地定义方法
子类型有时候需要覆盖超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。

function SuperType(){
    this.property = true;
}

SuperType.prototype.getSuperValue = function(){
    return this.property;
};

function SubType(){
    this.subproperty = false;
}

// 继承了 SuperType
SubType.prototype = new SuperType();

// 添加新方法
SubType.prototype.getSubValue = function (){
    return this.subproperty;
};

// 重写超类型中的方法
SubType.prototype.getSuperValue = function (){
    return false;
};

var instance = new SubType();
alert(instance.getSuperValue());   //false

还有一点需要注意的是,通过原型链实现继承时,不能使用对象字面量来创建原型,这样做会重写原型链。如下所示:

function SuperType(){
    this.property = true;
}

SuperType.prototype.getSuperValue = function(){
    return this.property;
};

function SubType(){
    this.subproperty = false;
}

// 继承了 SuperType
SubType.prototype = new SuperType();

// 使用字面量添加新方法,会导致上一行代码无效
SubType.prototype = {
    getSubValue : function (){
        return this.subproperty;
    },

    someOtherMethod : function (){
        return false;
    }
};

var instance = new SubType();
alert(instance.getSuperValue());   //error!
  • 原型链的问题
    原型链最主要的问题来自包含引用类型值的原型。包含引用类型值的原型属性会被所有实例共享;所以要在构造函数中定义属性,而不是在原型对象中定义。
    在通过原型实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就变成了现在的原型属性了。
function SuperType(){
    this.colors = ["red", "blue", "green"];
}

function SubType(){            
}

// 继承了 SuperType
SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors);    //"red,blue,green,black"

var instance2 = new SubType();
alert(instance2.colors);    //"red,blue,green,black"

上面例子中,SuperType构造函数定义了一个colors属性,SuperType的每个实例都会有各自包含自己数组的colors属性。当SubType通过原型链继承了SuperType之后,SubType.prototype就变成了SuperType的一个实例,因此它也拥有了一个它自己的colors属性 —— 就跟专门创建了一个SubType.prototype.colors属性一样。结果SubType的所有实例都会共享这一个colors属性。

原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。

鉴于这两个问题,实践中很少会单独使用原型链来实现继承。

3、作用域与作用域链

3.1、作用域

一个变量的作用域是程序源代码中定义这个变量的区域。

在之前,JavaScript是没有块级作用域的,并且还会出现变量提升的问题,导致内层变量可能会覆盖外层变量和用来计数的循环变量泄露为全局变量等问题,并且还出现了闭包的问题。
但是……但是!ES6 新增了let命令,let声明的变量,只在代码块内有效,并且也不存在变量提升。块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数(匿名 IIFE)不再必要了。

变量的作用域有全局作用域和函数作用域,还有ES6新增的块级作用域。

  • 全局作用域:全局变量拥有全局作用域,在任何地方都是有定义的。
// 全局作用域
var b = 10;
function a(){
  console.log('函数内的b:' + b);  // 函数内的b:10
}
a();
console.log('函数外的b:' + b);  // 函数外的b:10
  • 函数作用域:变量在声明他们的函数体以及这个函数体嵌套的任意函数体内都是有定义的。
// 函数作用域
function a(){
  var b = 10;
  console.log(b);  // 10
}
a();
console.log(b);  // Uncaught ReferenceError: b is not defined

// 函数作用域,变量声明提升
function a(){
  console.log(b);  // undefined
  var b = 10;
}
a();
console.log(b);  // Uncaught ReferenceError: b is not defined

// 相当于执行了以下代码
function a(){
  var b;
  console.log(b);  // undefined
  b = 10;
}
a();
console.log(b);  // Uncaught ReferenceError: b is not defined
  • 块级作用域:花括号内的变量有其自己的作用域,而且变量在声明它们的代码段之外是不可见的。
// 块级作用域
function a(){
  let b = 10;
  if (true) {
    let b = 20;
    console.log(b);  // 20
  }
  console.log(b);  // 10
}
a();

在以上代码中,分别用let在不同的花括号内声明了b,但是最终外层代码块不受内层代码块的影响。如果两次都使用var定义变量,则两个输出的值都是20

3.2、作用域链

当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途是保证对执行环节有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象作为变量对象。活动对象在最开始的时候只包含一个变量,即arguments对象(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。——《JavaScript高级程序设计》(第3版)

var color = "blue";
function changeColor(){
  if (color === "blue"){
    color = "red";
  } else {
    color = "blue";
  }
}
changeColor();
console.log("Color is now " + color);  // Color is now red

在上面例子中,函数changeColor()的作用域包含两个对象:它自己的变量对象(其中定义着arguments对象) 和全局环境的变量对象。最终输出为red,可见可以在函数内部访问变量color,就是因为可以在这个作用域链中找到它。
此外,在局部作用域中定义的变量可以在局部环境中与全局变量互换使用,见以下例子:

var color = "blue";

function changeColor(){
  var anotherColor = "red";
  function swapColors(){
    var tempColor = anotherColor;
    anotherColor = color;
    color = tempColor;
    // 这里可以访问color、anotherColor 和t empColor
  }
  // 这里可以访问color and anotherColor,但不能访问 tempColor        
  swapColors();
}
changeColor();
// 这里可以访问color,但不能访问anotherColor 和 tempColor
console.log("Color is now " + color);  // Color is now red

以上代码共涉及3个执行环境:全局环境、changeColor()的局部环境和swapColors()的局部环境。全局环境中有一个变量color和一个函数changeColor()changeColor()的局部环境中有一个名为anotherColor的变量和一个名为swapColors()的函数,但它也可以访问全局环境中的变量colorswapColors()的局部环境中有一个变量tempColor,该变量只能在这个环境中访问到。无论全局环境还是changeColor()的局部环境都无权访问tempColor。然而,在swapColors()内部则可以访问其他两个环境中的所有变量,因为那两个环境是它的父执行环境。

作用域链执行环境

上图中的矩形表示特定的执行环境。其中,内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。这些环境之间的联系是线性、有次序的。每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个执行环境。对于这个例子中的swapColors()而言,其作用域链包括3个对象:swapColors()的变量对象、changeColor()的变量对象和全局变量对象。swapColors()的局部环境开始时会现在自己的变量对象中搜索变量名和函数名,如果搜索不到则再搜索上一级作用域链。changeColor()的作用域链中指包含两个对象:它自己的变量对象和全局变量对象。也就是说,它不能访问swapColors()的环境。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354

推荐阅读更多精彩内容