欢迎光临
我们一直在努力

函数上下文、面向对象、原型和继承

一、 上下文

1.1 认识上下文

函数中的this到底指的是什么,就是函数的上下文的知识。

上下文,指的是前言后语,比如中文:

是不对的。

句子中,“这”你不知道表示的是什么,“这”这个字要通过前言后语来判断是啥, 称作“上下文”。

补全:

随地大小便,是不对的。

遛狗不栓绳,是不对的。

上下文的本质是什么呢? 答:进入函数内部的一种全新的方式。

长久以来,大家喜欢将对象作为函数的参数。也就是说,函数的第一种最常见的进入它内部的方法,就是使用参数

例:函数(对象)

// 学生小明
var xiaoming = {
    yuwen: 90,
    shuxue: 85,
    yingyu: 88
};

// 写一个函数,统计某学生的总分
function calcTotalScore(student) {
    return student.yuwen + student.shuxue + student.yingyu;
}

var totalScore = calcTotalScore(xiaoming);
console.log(totalScore);

我们长久以来,喜欢使用参数来进入函数内部。实际上,还有一种方法可以进入函数内部:上下文

例:对象.函数()

// 学生小明
var xiaoming = {
    yuwen: 90,
    shuxue: 85,
    yingyu: 88
};

// 写一个函数,统计某学生的总分
function calcTotalScore() {
    return this.yuwen + this.shuxue + this.yingyu;
}

// 给小明添加这个函数
xiaoming.calcTotalScore = calcTotalScore;
// 打点调用这个函数
var totalScore = xiaoming.calcTotalScore();
console.log(totalScore);

1.2 this 的到底是谁?

注意,this指的是什么,取决于如何被调用,而不是如何被定义。

比如下面这个经典案例,有一个对象叫做 obj,它有一个 a 属性,它的内部有一个 fn 函数,这个函数中有 this
谁说这个 this 就是 obj 呢??偏不!我可以提出来,var f = obj.fn; 此时f()语法就是直接圆括号调用,根据规则1,thiswindow对象。

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

// 函数只有执行了,才能揭示它的上下文是谁。
var f = obj.fn;

var a = 9; // 全局变量会被视为 window 的属性
// 下面调用函数了,这里不是"对象.函数()"的语法而是"函数()",所以上下文是 window
f();

this 指向规则

  • 规则1: 函数直接加圆括号运行,上下文是 window 对象。 例:fun()
  • 规则2: 对象打点调用函数,上下文是对象。 例:obj.fun()
  • 规则3: 数组中枚举出一项,上下文是数组。 例:arr[4]()
  • 规则4: 定时器调用函数,上下文是 window 对象。 例:setInterval(fun, 1000);
  • 规则5: IIFE调用函数,上下文是 window 对象。 例:(function(){})()
  • 规则6: 事件处理函数,上下文是绑定事件的这个元素。 例:oBox.onclick = fun;

例:

var obj = {
    a: 1,
    b: 2,
    c: function() {
        return {
            a: 3,
            b: 4,
            fn: function() {
                console.log(this.a);
            }
        }
    }
}

obj.c().fn();   // 3。规则2

var fn = obj.c().fn;
fn();           // undefined。规则1

obj.fn = obj.c().fn;    // 将函数写到obj对象上一份
obj.fn();               // 1。规则2

例:

function fun1(a, b, c, d) {
    arguments[0](7, 8, 9, 10, 11);
}

function fun2() {
    console.log(this.length); // 3    
    console.log(this.callee.length); // 4
    console.log(arguments.length); // 5
    console.log(arguments.callee.length); // 0
}

fun1(fun2, 5, 6);

语句 arguments[0](7,8,9,10,11);,构成了形式:数组下标;

  • 规则3生效,上下文就是这个数组。所以 fun2 的 this 表示 fun1 的 arguments 对象。arguments 对象就是函数的实参列表数组。

  • 注意,arguments.callee是一个固定写法,表示函数本身。函数的长度是形参列表的长度。

例:

var obj = {
    a: 3,
    fun: function() {
        var a = 5;
        // 一定别激动,必须看清楚函数如何被调用!
        return function() {
            alert(this.a);
        }
    }
}
var a = 7;

obj.fun()(); // 7。规则1:函数直接加圆括号调用。

例:

var obj = {
    a: 3,
    fun: (function() {
        var a = 5;
        // 一定别激动,必须看清楚函数如何被调用!
        return function() {
            console.log(this.a);
        }
    })()
}
var a = 7;

obj.fun(); // 3。规则2:对象打点调用函数,上下文是对象

1.3 call、apply 和 bind

callapplybind都能够指定函数的上下文。

  • call 和 apply 是临时指定
  • bind 是终身设置,bind 之后再也不能改,也不能再次 bind 了。

基本案例:

var obj = {
    a: 1,
    b: 2
}

function fun() {
    console.log(this.a + this.b);
}

fun.call(obj);
fun.apply(obj);

语法:

  • 函数.call(对象)
  • 函数.apply(对象)

这个对象就能够成为这个函数的上下文。

call 和 apply 有啥区别呢?

传递参数的时候,call 要传入一个个零散值,而 apply 要传入数组。

var obj = {
    a: 1,
    b: 2
}

function fun(c, d) {
    console.log(this.a + this.b + c + d);
}

fun.call(obj, 3, 4);
fun.apply(obj, [3, 4]);
fun.call(obj, ...[3, 4]);

所以 apply 有一个神奇的地方,就是能让数组变为零散值传入函数内部。

所以,以前有一个著名的算法,就是求数组最大值:系统内置了一个函数叫做Math.max()。不能传入数组,只能传入零散值。

它怎么用于求数组的最大值呢?? 答案就是:

Math.max.apply(null, [11, 2333, 25, 66])

现在很少用这个了,因为有...运算符了(ES6 中新增)。

var arr = [11, 2333, 25, 66];
console.log(Math.max.apply(null, arr));
console.log(Math.max(null, ...arr));

例:call 和 apply 是临时设置上下文,不是永远。如下:

var obj1 = {
    a: 1,
    b: 2
}

var obj2 = {
    a: 33,
    b: 44
}

function fun() {
    console.log(this.a + this.b);
}

fun.call(obj1); // 3
fun.call(obj2); // 77

ES6 中新增了一个bind()函数,将绑死上下文,将海枯石烂,天地可鉴。

bind()不会因为函数的调用,而call()apply()会引发函数调用。

var obj1 = {
    a: 1,
    b: 2
}

var obj2 = {
    a: 33,
    b: 44
}

function fun() {
    console.log(this.a + this.b);
}

fun = fun.bind(obj1); // 终身指定上下文,无视如何调用函数的

fun(); // 失效,这里看似规则1,实际上fun函数已经绑定死了上下文对象为obj1

fun.call(obj2); // 失效

fun = fun.bind(obj2); // 失效

fun.call(obj2); // 失效

面试题:

function getLength() {
    return this.length;
}

function foo() {
    this.length = 1;
    return (function() {
        var length = 2;
        return {
            length: function(a, b, c) {
                return this.arr.length;
            },
            arr: [1, 2, 3, 4],
            info: function() {
                return getLength.call(this.length);
            }
        }
    })();
}

var result = foo().info();
console.log(result);

答案是3。解释原理,见视频。

面试题:

function fun() {
    var a = 1;
    this.a = 2;

    function fn() {
        return this.a;
    }
    fn.a = 3;
    return fn;
}
alert(fun()());

答案是2。解释原理,见视频。

1.4 箭头函数

箭头函数的上下文不是取决于如何调用,而是取决于定义时候的外部上下文是啥。

所以,和function定义的函数正好拧巴着。

function fun() {
    return () => {
        console.log(this.a);
    }
}

var obj1 = { a: 3 };
var obj2 = { a: 88 };

// 以 obj1 为上下文调用 fun 函数,产生了箭头函数
// 此时箭头函数诞生的时的外部 this 是 obj1,所以箭头函数终身上下文是 obj1
var arrowFun = fun.call(obj1);

// 哪怕你直接调用这个箭头函数,不走规则1,上下文一定是obj1啊!
arrowFun();
// 怎么call别人都没用
arrowFun.call(obj2);

箭头函数看的是诞生时候,所处的 function 的上下文,而不是在哪个对象中。

比如下面这个题目出的有问题:

var obj = {
    a: 1,
    b: 2,
    fn: () => {
        console.log(this.a);
    }
}

obj.fn(); // undefined

箭头函数写在了一个对象中,根本没用!它必须写在一个函数中。

正确的题目:

var obj = {
    a: 1,
    b: 2,
    fn: function() {
        const arrawFun = () => {
            console.log(this.a);
        }
        return arrawFun;
    }
}

obj.fn()();

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

二、面向对象和原型

2.1 用 new 调用函数

当用 new 调用函数,执行四步走:

  • ① 秘密创建空对象;
  • ② 将 this 绑定当这个对象上;
  • ③ 执行函数;
  • ④ 返回 this;

基本案例:

function fun(a, b, c) {
    this.a = a;
    this.b = b;
    this.c = c;
}

var o = new fun(33, 44, 55);

console.log(o); // {a : 33, b : 44, c: 55}

现在人们发现用 new 调用函数,总是能够产生有相同属性群的对象。所以人们说这些对象,是同一类对象的实例。

这个函数叫做构造函数,也叫作“类”。构造函数的名字建议首字母大写。

注意:一个函数是不是构造函数,不是看有没有大写开头,而是看有没有用 new 来调用。

例:

function People(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
}
var xiaoming = new People("小明", 12, "男");
var xiaohong = new People("小红", 11, "女");
console.log(xiaoming);
console.log(xiaohong);

2.2 原型

构造函数的prototype,是每个new出来的实例的原型。

function People(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
}
People.prototype.sayHello = function() {
    console.log(`你好,我是${this.name},我今年${this.age}岁了`);
}

var xiaoming = new People("小明", 12, "男");
xiaoming.sayHello();

用图表示就是一个三角关系:

原型

构造函数天生具有 prototype 对象,这个对象平时不显山不漏水,感觉不存在,感觉没啥用。
但是,当这个函数成为了构造函数的时候,这个 Prototype 对象就成为了函数 new 出的实例的原型。
而原型,有原型链查找的能力。

证明原型链存在:

console.log(xiaoming.hasOwnProperty("name"));       // true
console.log(xiaoming.hasOwnProperty("age"));        // true
console.log(xiaoming.hasOwnProperty("sex"));        // true
console.log(xiaoming.hasOwnProperty("sayHello"));   // false

console.log(xiaoming.__proto__.hasOwnProperty("sayHello"));   //true
console.log(xiaoming.__proto__ === People.prototype);   //true

为了能够复用方法,必须写在构造函数的原型上,如果写在自己身上,太浪费内存。

平时我们用面向对象写东西,形成了一个基本语法:

function 类名(属性1, 属性2, 属性3) {
    this.属性1 = 属性1;
    this.属性2 = 属性2;
    this.属性3 = 属性3;
}
类名.prototype.方法1 = function() {

}
类名.prototype.方法2 = function() {

}

2.3 原型链

任何东西都有原型。必须对下面的这个图特别的熟悉:

原型链

证明一下原型链:

console.log(Object.__proto__ === Function.prototype);           //true
console.log(Object.__proto__.__proto__ === Object.prototype);   //true
console.log(Function.__proto__ === Function.prototype);         //true

学习原型链之后,就能够拓展一些类型的能力了。

比如,给数组赋予一些能力,就要写在Array.prototype上即可。

例:给数组对象增加寻找最大值的功能:

// 拓展了数组
Array.prototype.findMax = function() {
    return Math.max(...this);
}

var arr = [1, 2, 3, 4, 5, 3];
var max = arr.findMax();

console.log(max);

例:给字符串增加 xiuzheng() 方法,就是前后的空格都去掉:

String.prototype.xiuzheng = function() {
    return this.replace(/^(\s+)|(\s+)$/g, "");
}
var str = "     我爱你    ";

console.log(str.xiuzheng());
console.log(str.xiuzheng().length);

2.4 ES6中的面向对象

ES6新增了class关键字,定义类,但是本质上仍然是构造函数,以及构造函数的原型。如下例子:

class People {
  // 构造函数
  constructor(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
  }
  sayHello() {
    console.log(`你好,我是${this.name},我今年${this.age}岁了`);
  }
}

var xiaoming = new People("小明", 12, "男");
xiaoming.sayHello();

千万不要以为JS中真的就有类的概念了,本质上仍然是构造函数和prototype那些事儿。

ES6自动将罗列的方法,写到构造函数的原型上了。

因此我们可以说,ES6中这个class关键字是一个语法糖,机理本质上根本就没有变化,变化的是语法而已。我们可以通过以下方式验证:

console.log(xiaoming.__proto__.hasOwnProperty("sayHello")); // true
console.log(xiaoming.__proto__ == People.prototype); // true
console.log(typeof People); // "function"

三、继承

计算机科学中,如果A类的属性(方法)群,能够完全包裹另B类的属性(方法)群,称:

A继承于B;A叫做子类,B叫做父类(超类)。

继承关系

属性(方法)群的圈圈更大,说明描述这个类的更加具体,导致这个类的实例数量一定会减少。

例:利用原型链,轻松实现继承。

原型链,实现继承

// 超类,人类
function People(name, age, sex) {
  this.name = name;
  this.age = age;
  this.sex = sex;
}
People.prototype.sayHello = function() {
  console.log(`你好,我是${this.name},我今年${this.age}岁了`);
}


// 子类,学生类
function Student(name, age, sex, xuehao, banji) {
  // 从父类那里得到一些属性
  People.call(this, name, age, sex);
  this.xuehao = xuehao;
  this.banji = banji;
}
// 重点语句来了
Student.prototype = new People();
// 补方法
Student.prototype.kaoshi = function() {
  console.log(`我是${this.name},我在考试`);
}

var xiaodoubao = new Student("小豆包", 8, "男", "100001", "一年级一班");
xiaodoubao.sayHello();
xiaodoubao.kaoshi();

例:ES6中新增了extends关键字,轻松实现继承。

// class 声明创建一个基于原型继承的具有给定名称的新类。
class People {
  // 构造方法,constructor 是一种用于创建和初始化class创建的对象的特殊方法。
  constructor(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
  }
  sayHello() {
    console.log(`你好,我是${this.name},我今年${this.age}岁了`);
  }
}

class Student extends People {
  constructor(name, age, sex, xuehao, banji) {
    // 调用父类的构造方法。
    super(name, age, sex);
    // 注意:1、super() 只能在构造函数中使用;2、在派生类中, 必须先调用 super() 才能使用 "this"。忽略这个,将会导致一个引用错误。
    this.xuehao = xuehao;
    this.banji = banji;
  }
  kaoshi() {
    console.log(`我是${this.name},我在考试`);
  }
}

var xiaodoubao = new Student("小豆包", 8, "男", "100001", "一年级一班");
xiaodoubao.sayHello();
xiaodoubao.kaoshi();

但是机理没有变化,仍然是那个“原型链上去”的图。

console.log(xiaodoubao.__proto__.__proto__ == People.prototype);

还有一种小的手段可以实现继承,就是Object.create();

Object.create(对象),将以这个对象为原型创建新对象。如下例子:

var obj1 = {
  a: 1,
  b: 2
};

var obj2 = Object.create(obj1);

console.log(obj2.a); // 1
console.log(obj2.b); // 2
console.log(obj2.hasOwnProperty("a")); // false
console.log(obj2.hasOwnProperty("b")); // false
console.log(obj2.__proto__ == obj1); // true

经典面试题就是让你自己写一个函数实现Object.create()方法。

答案就是这个:

function fun(o) {
  function f() {

  }
  f.prototype = o;
  return new f();
}

秘密创建一个函数,让这个函数的prototype指向o,它new出来的实例原型链不就指向o了么。

参考资料:

适合初学者的JavaScript面向对象 — MDN

赞(2) 打赏
未经允许不得转载:前端学习分享网 » 函数上下文、面向对象、原型和继承

评论 抢沙发

评论前必须登录!

 

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏