this & 原型链 & 继承

this

在全局作用域下,this代表window

<script>
    console.log(this)
</script>
//如果在全局下这样写
var a = 1
var b = 2

//就相当于window的属性
window.a
//返回1
window.b
//返回2
window.a === this.a  //true

作为函数调用

在函数被直接调用时this绑定到全局对象。在浏览器中,window 就是该全局对象

console.log(this);

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

fn1();
//两个this都是window

相当于:
var a = 1
function fn1(){
    console.log(a)
}
fn1()
//调用fn1,输出a,函数中没有a的值,去外面查找,找到了a = 1


function fn1(){
    var b = 2
    console.log(this.b)
}
fn1()
//这样输出undefined,因为全局下没有b

内部函数

函数嵌套产生的内部函数的this不是其父函数,仍然是全局变量

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

fn0();

setTimeout、setInterval

这两个方法执行的函数this也是全局对象

document.addEventListener('click', function(e){
    console.log(this);  //这个this是document
    setTimeout(function(){
        console.log(this);  //这个this是全局对象
    }, 200);
}, false);

如果想使用上面的this
document.addEventListener('click', function(e){
    console.log(this);  //这个this是document
    var _this = this
    setTimeout(function(){
        console.log(_this);  //就是上面的this了
    }, 200);
}, false);

作为构造函数调用

所谓构造函数,就是通过这个函数生成一个新对象(object)。这时,this就指这个新对象

new 运算符接受一个函数 F 及其参数:new F(arguments...)。这一过程分为三步:

  • 创建类的实例。这步是把一个空的对象的 proto 属性设置为 F.prototype 。
  • 初始化实例。函数 F 被传入参数并调用,关键字 this 被设定为该实例。
  • 返回实例。
    看例子
function Person(name){
    this.name = name;
}
Person.prototype.printName = function(){
    console.log(this.name);
};

var p1 = new Person('Byron');
var p2 = new Person('Casper');
var p3 = new Person('Vincent');

p1.printName();
p2.printName();
p3.printName();

作为对象方法调用

在 JavaScript 中,函数也是对象,因此函数可以作为一个对象的属性,此时该函数被称为该对象的方法,在使用这种调用方式时,this 被自然绑定到该对象

var obj1 = {
    name: 'Byron',
    fn : function(){
        console.log(this);
    }
};

obj1.fn();  //this是obj1,谁调用,this就是谁

var obj2 = {
    name: 'Byron',
    obj3:{
        fn : function(){
            console.log(this);
        }
    }
}
obj2.obj3.fn();  //this是obj3

//小陷阱
var fn2 = obj1.fn

fn2()  //fn2是全局变量,相当于widow.fn2(),所以this代表window  

DOM对象绑定事件

在事件处理程序中this代表事件源DOM对象(低版本IE有bug,指向了window)

document.addEventListener('click', function(e){
    console.log(this);  //this是document
    var _document = this;
    setTimeout(function(){
        console.log(this);  //window
        console.log(_document);  //document
    }, 200);
}, false);

Function.prototype.bind

bind,返回一个新函数,并且使函数内部的this为传入的第一个参数

var obj1 = {
    name: 'Byron',
    fn : function(){
        console.log(this);
    }
};
var obj3 = { a:3 };
var fn3 = obj1.fn.bind(obj3);
fn3();  //obj3

//改变一下代码,让setTimeout中的this也是document
document.addEventListener('click', function(e){
    console.log(this);  //this是document
    setTimeout(function(){
        console.log(this);  //this是document
    }.bind(this), 200);   //添加bind(this),bind得到一个新的函数,函数里面的this,是传递的对象
}, false);

使用call和apply设置this

call apply,调用一个函数,传入函数执行上下文及参数

fn.call(context, param1, param2...)

fn.apply(context, paramArray)

语法很简单,第一个参数都是希望设置的this对象,不同之处在于call方法接收参数列表,而apply接收参数数组

fn2.call(obj1);
fn2.apply(obj1);
举例 
var value = 100
function fn4(a,b){
    console.log(this.value + a + b) //this是全局的window
}
fn4(3,4)  //107
//----------------------
var obj4 = {
    value: 200
}
function fn4(a,b){
    console.log(this.value + a + b)
}
//call的用法
fn4.call(obj4,3,4)   //this就是obj4,第一个参数
//apply的用法
fn4.apply(obj4,[3,4])  //把后面数组的东西,作为函数的参数

求最大值

var arr = [1,2,7,4]
Math.max(1,2,7,4)  //这样才能得到最大值
使用apply方法实现
console.log(Math.max.apply(null,arr))

arguments

arguments是一个类数组对象,不是一个数组,没有数组的方法。

[].join()    //数组里面有个join的方法。

如果这样写
function joinStr(){
    return arguments.join('-')
}
joinStr('a','b','c')
会报错,arguments不是一个函数,是一个对象。没有join方法

正确方法在下面

在函数调用时,会自动在该函数内部生成一个名为 arguments的隐藏对象

该对象类似于数组,可以使用[]运算符获取函数调用时传递的实参

只有函数被调用时,arguments对象才会创建,未调用时其值为null

 function fn5(name, age){
     console.log(arguments);
     name = 'XXX';
     console.log(arguments);
     arguments[1] = 30;
     console.log(arguments);
 }
 fn5('Byron', 20);
方法一
function joinStr(){
    console.log(Array.prototype.join.call(arguments,'-'))  //join() 将数组转化为字符串
    //[].join.call === Array.prototype.call  没有区别。[]是Array的一个实例。
}
joinStr('a','b','c')   //a-b-c

方法二
function joinStr(){
    var join = Array.prototype.join.bind(arguments)
    console.log(join('-'))
}
joinStr('a','b','c')   //a-b-c

函数的执行环境

JavaScript中的函数既可以被当作普通函数执行,也可以作为对象的方法执行,这是导致 this 含义如此丰富的主要原因

一个函数被执行时,会创建一个执行环境(ExecutionContext),函数的所有的行为均发生在此执行环境中,构建该执行环境时,JavaScript 首先会创建 arguments变量,其中包含调用函数时传入的参数

接下来创建作用域链,然后初始化变量。首先初始化函数的形参表,值为 arguments变量中对应的值,如果 arguments变量中没有对应值,则该形参初始化为 undefined。

如果该函数中含有内部函数,则初始化这些内部函数。如果没有,继续初始化该函数内定义的局部变量,需要注意的是此时这些变量初始化为 undefined,其赋值操作在执行环境(ExecutionContext)创建成功后,函数执行时才会执行,这点对于我们理解JavaScript中的变量作用域非常重要,最后为this变量赋值,会根据函数调用方式的不同,赋给this全局对象,当前对象等

至此函数的执行环境(ExecutionContext)创建成功,函数开始逐行执行,所需变量均从之前构建好的执行环境(ExecutionContext)中读取

三种变量

  • 实例变量:(this)类的实例才能访问到的变量

  • 静态变量:(属性)直接类型对象能访问到的变量

  • 私有变量:(局部变量)当前作用域内有效的变量

看个例子

function ClassA(){
    var a = 1; //私有变量,只有函数内部可以访问
    this.b = 2; //实例变量,只有实例可以访问
}

ClassA.c = 3; // 静态变量,也就是属性,类型访问

console.log(a); // error
console.log(ClassA.b) // undefined
console.log(ClassA.c) //3

var classa = new ClassA();
console.log(classa.a);//undefined
console.log(classa.b);// 2
console.log(classa.c);//undefined

更多关于 this 的文章


原型链

回顾一下类、实例、prototype、__proto__的关系

function Person(nick, age){
    this.nick = nick;
    this.age = age;
}
Person.prototype.sayName = function(){   //函数都有一个prototype属性,这个属性是一个对象。sayName是绑定的方法。
    console.log(this.nick);
}

var p1 = new Person('Herbert',25);
var p1 = new Person('Jack',25);

//new分三步
//创建一个新的对象
//执行这个函数,里面的this代表刚刚创建的对象
//把刚刚创建的对象返回。
//所有对象的__proto__都指向prototype

p1.sayName();
p2.sayName();
  • 我们通过函数定义了类Person,类(函数)自动获得属性prototype
  • 每个类的实例都会有一个内部属性proto,指向类的prototype属性
p1.__proto__.constructor === Person  //true
Person.prototype.constructor === Person  //true
p1.constructor === Person  //true  发现本身没有,就会从__proto__中查找。
p1.toString()    //[object Object]  这个属性又是从哪里来的?
//从p1.__proto__.__proto__中找到的  (原型链)
如果给他赋值,就会覆盖,再调用时就直接获取,不去__proto__中查找
p1.toString = function(){congsole.log(this)}

有趣的现象

我们定义一个数组,调用其valueOf方法

var a = [1,2,3]  //字面量创建数组
new Array(1,2,3)  //一样的效果  Array是一个构造函数,1,2,3是传的参数
a.join('--')   //"1--2--3"  哪来的?就是Array.prototype里面的

[1, 2, 3].valueOf(); // [1, 2, 3]

很奇怪的是我们在数组的类型Array中并不能找到valueOf的定义,根据之前的理论那么极有可能定义在了Array的prototype中用于实例共享方法,查看一下

我们发现Arrayprototype里面并未包含valueOf等定义,那么valueOf是哪里来的呢?

一个有趣的现象是我们在Object实例的__proto__属性(也就是Object的prototype属性)中找到了找到了这个方法

那么Array的实例为什么同样可以查找到Object的prototype里面定义的方法呢?

查找valueOf过程

因为任何类的prototype属性本质上都是个类Object的实例,所以prototype也和其它实例一样也有个proto内部属性,指向其类型Object的prototype

我们大概可以知道为什么了,自己的类的prototype找不到的话,还会找prototype的类型的prototype属性,这样层层向上查找

大概过程是这样的

  • 记当前对象为obj,查找obj属性、方法,找到后返回

  • 没有找到,通过obj的proto属性,找到其类型Array的prototype属性(记为prop)继续查找,找到后返回

  • 没有找到,把prop记为obj做递归重复步骤一,通过类似方法找到prop的类型Object的 prototype进行查找,找到返回


这就是传说中的原型链,层层向上查找,最后还没有就返回undefined

类型

instanceof操作符,判断一个对象是不是某个类型的实例

[1, 2, 3] instanceof Array; //true
//判断[1, 2, 3].__proto__是否等于Array.prototype
//如果不是。再看[1, 2, 3].__proto__.__proto__是否等于Array.prototype
//一直往下找,直到找到,或者__proto__为null

可以看到[1, 2, 3]是类型Array的实例

[1, 2, 3] instanceof Object; //true

这个结果有些匪夷所思,怎么又是Array的实例,又是Object的实例,这不是乱套了
其实这个现象在日常生活中很常见其实,比如我们有两种类型

  • 类人猿
  • 动物
    我们发现黑猩猩既是类人猿这个类的物种(实例),也是动物的实例
    是不是悟出其中的门道了,类人猿是动物的一种,也就是说我们的两个类型之间有一种父子关系
    这就是传说中的继承,JavaScript正是通过原型链实现继承机制的

继承

继承是指一个对象直接使用另一对象的属性和方法。
JavaScript并不提供原生的继承机制,我们自己实现的方式很多,介绍一种最为通用的
通过上面定义我们可以看出我们如果实现了两点的话就可以说我们实现了继承

  • 得到一个类的属性
  • 得到一个类的方法

分开讨论一下,先定义两个类

function Person(name, age){
    this.name = name;
    this.age = age;
}

Person.prototype.printName = function(){
    console.log(this.name);
};


function Male(sex){
    this.sex = sex;
}

Male.prototype.printAge = function(){
    console.log(this.age);
};
假设需要定义一个角色,如果把基本属性都写一遍很繁琐。如果以后基本属性改变,这个角色也需要更改。
使用继承的原因:在原来定义好的基础上,增加一点自己的东西。需要借用以前的东西。
例子:
function Person(name,age){
    this.name = name
    this.age = age
}

//可以说话
Person.prototype.sayName = function(){
    console.log('My name is ' + this.name)
}

//可以行走
Person.prototype.walk = function(){
    console.log(this.name + ' is walking')
}

//构造一个人
var p = new Person('jack',20)


function Student(name,age,sex){
    第一步:把属性继承过来
    //Person.call(this,name,age)  //和下面方法一样
    Person.bind(this)(name,age)  //执行Person函数 重点! 这种方法只能继承属性,不能继承方法。

    第二步:继承方法
    //Student.prototype.__proto__ = Person.prototype  使用下面的方法
    this.sex = sex
}

//继承方法
Student.prototype = Object.create(Person.prototype)
//如果不使用上面这句,可以使用以下代码代替,要注意次序,在下面代码之前。
//fn.prototype = Person.prototype
//function fn(){}
//Student.prototype = new fn()

Student.prototype.constructor = Student  //指向自身,不写则指向Person

//s instanceof Student    true
//s instanceof Person     true
//s.__proto__.__proto__ === Person.prototype   true

//增加的新属性
Student.prototype.doing = function(){
    console.log('I an studing')
}

var s = new Student('mack',2,'boy')

属性获取

对象属性的获取是通过构造函数的执行,我们在一个类中执行另外一个类的构造函数,就可以把属性赋值到自己内部,但是我们需要把环境改到自己的作用域内,这就要借助我们讲过的函数call了

改造一些Male

function Male(name, sex, age){
    Person.call(this, name, sex);
    this.age = age;
}

Male.prototype.printAge = function(){
    console.log(this.age);
};
实例化看看结果

var m = new Male('Byron', 'male', 26);
console.log(m.sex); // "male"

方法获取

我们知道类的方法都定义在了prototype里面,所以只要我们把子类的prototype改为父类的prototype的备份就好了

Male.prototype = Object.create(Person.prototype);
//Object.create() 方法会使用指定的原型对象及其属性去创建一个新的对象。

Object.create() - JavaScript | MDN

例子:
var a = Object.create({b:1})  //创建一个对象,对象的原型是{b:1}   
a   //输出Object,里面什么都没有,在__proto__下有b:1

这里我们通过Object.createclone了一个新的prototype而不是直接把Person.prtotype直接赋值,因为引用关系,这样会导致后续修改子类的prototype也修改了父类的prototype,因为修改的是一个值

另外Object.create是ES5方法,之前版本通过遍历属性也可以实现浅拷贝

这样做需要注意一点就是对子类添加方法,必须在修改其prototype之后,如果在之前会被覆盖掉

Male.prototype.printAge = function(){
    console.log(this.age);
};

Male.prototype = Object.create(Person.prototype);

这样的话,printAge方法在赋值后就没了,因此得这么写

function Male(name, sex, age){
    Person.call(this, name, sex);
    this.age = age;
}

Male.prototype = Object.create(Person.prototype);

Male.prototype.printAge = function(){
    console.log(this.age);
};

这样写貌似没问题了,但是有个问题就是我们知道prototype对象有一个属性constructor指向其类型,因为我们复制的父元素的prototype,这时候constructor属性指向是不对的,导致我们判断类型出错

Male.prototype.constructor; //Person

因此我们需要再重新指定一下constructor属性到自己的类型

最终方案

我们可以通过一个函数实现刚才说的内容

function inherit(superType, subType){
    var _prototype  = Object.create(superType.prototype);
    _prototype.constructor = subType;
    subType.prototype = _prototype;
}

使用方式

function Person(name, sex){
    this.name = name;
    this.sex = sex;
}

Person.prototype.printName = function(){
    console.log(this.name);
};    

function Male(name, sex, age){
    Person.call(this, name, sex);
    this.age = age;
}
inherit(Person, Male);

// 在继承函数之后写自己的方法,否则会被覆盖
Male.prototype.printAge = function(){
    console.log(this.age);
};

var m = new Male('Byron', 'm', 26);
m.printName();

这样我们就在JavaScript中实现了继承

hasOwnProperty

可能会有一个疑惑,继承之后Male的实例也有了Person的方法,那么怎么判断某个是自己的还是父类的?

hasOwnPerperty是Object.prototype的一个方法,可以判断一个对象是否包含自定义属性而不是原型链上的属性,hasOwnProperty是JavaScript中唯一一个处理属性但是不查找原型链的函数

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

推荐阅读更多精彩内容