moonshot 1 - 原型 闭包

201 阅读5分钟

原型

试想,我们定义了一个对象animal用来描述动物。显然,这个对象中需要有一些动物理应拥有的特性,例如体重、年龄,当然它们还可以奔跑以及进食。我们把这些特性写成属性的形式,如下

const animal = {
    weight: '71kg',
    age: '7',
    run() {
        alert("See that?? I can run!!!");
    },
    eat() {
        alert("Now I'm hungury, I can eat an elephant");
    }
}

不可否认,兔子是一种典型的动物,我们同样用一个对象rabbit来描述兔子,这个rabbit对象固然可以把上面代码中的所有属性都CV过来,但这样很蠢。

事实上,我们可以直接利用__proto__来让animal成为rabbit原型,这样rabbit就可以直接使用animal里的所有属性!

const rabbit = {
    jump() {
        alert("See that?? I can jump!!!");
    },
    __proto__: animal // 这句代码就把 animal 设置成 rabbit 的原型对象
}
// 以下代码都可以正常运行
rabbit.eat();
rabbit.run();
console.log(rabbit.weight);

至此,可以回答以下几个问题:

  • 什么是原型,以及如何使用原型 原型,也即原型对象,就是一个对象!之所以叫原型对象,是因为它被别的对象当作了原型。

实际上,每一个对象内部都有一个[[Prototype]]属性,它只可能有两种值,一种是null,另一种就是指向一个对象,[[Prototype]]所指向的那个对象就是原型对象!

但是,我们不能直接用类似someObj.[[Prototype]] = anthorObj;这样的代码来为一个对象设置原型,这是完全错误的,因为[[Prototype]]仅存在于每个对象的内部并且是隐藏的,它只是存在于那里,你也可以通过浏览器的控制台确确实实地看到它,但你就是不能使用它。

不过没关系,我们还有__proto__可以使用,它的本质就是[[Prototype]]的getter/setter方法,你可以使用someObj.__proto__ = anthorObj;这样的代码来为一个对象设置对象,本质上也就是通过它来改变了[[Prototype]]的指向。

但是我们之所以还在使用__proto__来设置原型,这是JavaScript的历史遗留问题,我们不做探讨。但更规范的写法是用下面两个API

const animal = {...};
const rabbit = {...};
Object.setPrototypeOf(rabbit,animal); // 将rabbit的原型设置成animal
Object.getPrototypeOf(rabbit); // 读取rabbit此时的原型对象,显然是animal
  • 构造函数与原型 我们可以通过构造函数来创建一个对象
// 构造函数
function Man(name) {
    this.sex = 'male';
    this.name = name;
};
// 创建一个对象
let me = new Man('lee');

如果此时我们想把people对象作为原型,并且是作为所有用new Man创建出来的对象的原型

const people = {
    sleep() {
        alert('ZZZ');
    }
};
function Man(name) {...};
// 把 Man 的所有实例化对象的原型对象设置为 people
Man.prototype = people;
let me = new Man('lee');
me.sleep(); //正常运行

可见,构造函数里面内置了一个名为prototype的属性,可以通过它来设定一个原型,并且在搭配new创建实例化对象之后(注意必须搭配new使用才行),这些实例化对象都具有了同一个原型。

  • 原型链 我们以JS中内置对象为例子来说明原型链

数组Array,函数Function,数字Number都是内置对象,用这些内置对象实例化出来的对象,也当然是对象

let arr = new Array([1,2,3]);
let fun = function(args) {...};
let num = new Number(5);

既然是对象就一定有[[Prototype]]属性,它们的[[Prototype]]全都指向各自的那个内置对象,这些内置对象里有着很多JS开发人员写好的属性和方法供每一个实例化的对象使用。

而这些内置对象又都指向了同一个原型对象——Object,它本身也是一个对象,因此也还是有[[Prototype]]属性,但是它的[[Prototype]]属性指向的就是null了。 QQ图片20211230165208.png

  • 一些注意事项
  1. 一个对象只能设置一个原型对象,多次设置原型则前面的原型会被后面的覆盖。但一个对象可以被另外多个对象当作原型。
  2. 原型链不能构成闭环,否则会报错。
  3. 若原型里的方法使用到了this,那要注意是谁用.运算符调用了这个方法,this就指向谁。这一点是合乎情理的,原型里的属性和方法只提供给别的对象使用,但不能在使用的时候把原型上原有的内容给更改了。

闭包

  • 什么是闭包 闭包是有权访问另一个函数作用域中局部变量的函数

这个定义给的有些过于抽象,我们举个简单的例子

function func() {
    let num = 7;
    function demo() {
        alert(num);
    };
    demo();
};
func();

很显然,demo函数可以访问func函数中的局部变量num,这就满足了闭包的定义,我们完全可以说demo就是一个闭包函数!

当然,上述代码还可以写得更加简单

function func() {
    let num = 7;
    return function() {
        alert(num);
    };
};
let foo = func();
foo();

很显然,我们定义了一个在func外面的函数foo,它也可以访问func中的局部变量num,我们同样也可以说foo是一个闭包函数。

  • 闭包的作用 一句话概括,闭包的主要作用就是延伸了变量的作用范围。

原本上述例子中的局部变量num理论上应该只能被func使用,但由于闭包的存在,它又可以被demofoo来使用,这就是把num这个变量的作用范围扩大了。