原型
试想,我们定义了一个对象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了。
- 一些注意事项
- 一个对象只能设置一个原型对象,多次设置原型则前面的原型会被后面的覆盖。但一个对象可以被另外多个对象当作原型。
- 原型链不能构成闭环,否则会报错。
- 若原型里的方法使用到了
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使用,但由于闭包的存在,它又可以被demo和foo来使用,这就是把num这个变量的作用范围扩大了。