原型和原型链
- 所有对象都是通过
new 函数创建 - 所有的函数也是对象
- 函数中可以有属性,方法也可以叫做属性。
- 所有对象都是引用类型
原型 prototype
所有函数都有一个属性:prototype,称之为函数原型。函数原型自然主语自然是函数,只要函数对象才有的属性,非函数对象没有这个属性。
默认情况下,prototype是一个普通的Object对象
默认情况下,prototype中有一个属性,constructor,它也是一个对象,它指向构造函数本身。
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype.motherland = 'China'
//这是一条很重要的准则 Person.prototype.constructor == Person
准则1:原型对象(即Person.prototype)的constructor指向构造函数本身
一个创建对象的问题
// 创建的是test对象
function test(){
}
var obj = new test();
// 创建的是Object对象,和下面的代码同样的效果
function test(){
return {};
}
function test(){
return new Object();
}
var obj = new test();
隐式原型 proto
所有的对象都有一个属性:__proto__,称之为隐式原型
默认情况下,隐式原型指向(创建该对象的函数)的原型。也就是可以理解为他是通过哪个函数创建的,当前对象的__proto__就指向该函数的原型。
当访问一个对象的成员时:
- 看该对象自身是否拥有该成员,如果有直接使用
- 在原型链中依次查找是否拥有该成员,如果有直接使用
猴子补丁:在函数原型中加入成员,以增强起对象的功能,猴子补丁会导致原型污染,使用需谨慎。
原型链
特殊点:
- Function的__proto__指向自身的prototype
- Object的prototype的__proto__指向null
下面几条
1.对象都是由函数创建,函数(也是对象)都是由Function函数来创建的。


2.每一个函数都有原型对象,即每一个函数对象都有一个property属性,property属性也是一个对象。而property这个属性对象里面都有一个constructor,而constuctor指向当前函数本身,也就是函数名称。


3.所有普通对象和函数对象都有__proto__这个隐式原型,(注意和函数对象有property这个原型做对比),也就是普通对象和函数对象都有 __proto__隐式原型,而只有函数对象有property这个原型。
而隐式原型代表的意思是创建该对象的函数原型。__proto__获取的都是函数的原型。
- 注意的是自定义函数是有Function函数创建的
- 注意Function的隐式原型指向Object,但是Object 却是 Function new 出来的

现在我们知道了,当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会通过它的__proto__隐式属性,找到它的构造函数的原型对象,如果还没有找到就会再在其构造函数的prototype的__proto__中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链。
其实就是先找自身的,自身找不到,就找他的隐式原型里面找,隐式原型找不到就去找隐式原型的隐式原型去找。隐式原型其实就是创建他这个对象的构造函数的原型。
覆盖的问题
array 在构造函数的原型上重写tostring的方法,那么在Object对象上的原型就没有机会被调用了,这就是覆盖的问题,类似oc里面的重写了某个方法。 如果在array使用object的方法,应该怎么做呢?
1.先找到原来tostring方法的实现
2.使用call改变this指向
具体如下图参考的那样
原型,隐式原型的面试题
1.第一题
var F = function () {}
Object.prototype.a = function () {}
Function.prototype.b = function () {}
var f = new F();
console.log(f.a, f.b, F.a, F.b);
// fn undefined fn fn
解读:可以看到上面的代码第二行给Object的原型添加了属性a,Function的原型添加了属性b。
f是自定义对象,他的原型链是自定义对象--->自定义函数的原型---->Object原型---> null
F是自定义函数,他的原型链是 自定义函数--->Function原型--->Object原型--->null
由于不论自定义函数还是自定义对象,他们的原型链上都有Object原型,所以,他们两个都有a这个属性。
而b属性是在Function原型上,所以只有自定义函数上面有,所有只有F.b有。
特别注意区别的是F.prototype 和 Function.prototype的区别,一个是自定义函数的原型,一个是Function的原型。
2.第二题
function A() {}
function B(a) {
this.a = a;
}
function C(a) {
if (a) {
this.a = a;
}
}
A.prototype.a = 1;
B.prototype.a = 1;
C.prototype.a = 1;
console.log(new A().a); //1
console.log(new B().a); //undefined
console.log(new C(2).a); //2
解读:
先查找自身的属性,自身的属性没有,会去隐式原型去找,隐式原型继续顺着原型链去找。
new A().a 原型上可以找到是1.
new B().a 自身属性可以找到是undefined.
new C(2).a 自身属性可以找到是2,没还没有轮到去原型链找。
3.第三题
function User() {}
User.prototype.sayHello = function() {}
var u1 = new User();
var u2 = new User();
console.log(u1.sayHello === u2.sayHello); //true
console.log(User.prototype.constructor); //User Function
console.log(User.prototype === Function.prototype); // false
console.log(User.__proto__ === Function.prototype); // true
console.log(User.__proto__ === Function.__proto__); // true
console.log(u1.__proto__ === u2.__proto__); // true
console.log(u1.__proto__ === User.__proto__); // false
console.log(Function.__proto__ === Object.__proto__); // true
console.log(Function.prototype.__proto__ === Object.prototype.__proto__); // false
console.log(Function.prototype.__proto__ === Object.prototype); // true
解读:
u1.sayHello === u2.sayHello均是对象的隐式原型上找到的,是同一个User.prototype.constructor构造函数的原型里面的constructor 指向构造函数本身User.prototype === Function.prototype左边是自定义构造函数的原型,右边是Function 的原型。User.__proto__ === Function.prototype左边是自定义构造函数的隐式原型,可以理解为谁创建了自定义构造函数,是Function创建了自定义构造函数,User.__proto__就代表自定义构造函数的隐式原型是Function函数的原型。User.__proto__ === Function.__proto__,这里需要注意一点的是Function的原型和隐式原型都指向Function的原型。所以这里和4是一样的结果。u1.__proto__ === u2.__proto__左右两边其实都是User的原型Function.__proto__ === Object.__proto__左边其实就是Function的原型,Object的构造函数是由Function创建的,因此Object.__proto__的隐式原型其实就是Function的原型.Function.prototype.__proto__ === Object.prototype.__proto__左边其实代表的是Function的原型的隐式原型,其实就是Object的原型,Object.prototype.__proto__其实是null。- 看8的解释。
静态方法,原型方法,实例方法

// 构造函数
function People(color) {
this.color = color;
this.seeColor = function() {
console.log("color is "+this.color)
}
}
// 静态方法
People.say = function() {
console.log("静态方法color is ", this.color)
}
// 原型方法
People.prototype.eat = function() {
console.log("原型方法color is ", this.color)
}
// 实例
var pp = new People();
// 调用静态方法
People.say(); // 静态方法color is undefined
pp.say(); // 报错信息:Uncaught TypeError: pp.say is not a function
// 调用原型方法
People.eat(); // 报错信息:Uncaught TypeError: People.eat is not a function
pp.eat(); // 原型方法color is Red
// 调用实例方法
People.seeColor(); // Uncaught TypeError: People.seeColor is not a function
pp.seeColor(); // color is Red
总结:
1.实例方法,其实就是普通对象的方法,他的方法只能被普通对象所访问。
2.静态方法,可以理解为People这个类的方法,可以类比为类方法,这个只能被People这个函数对象调用,不能被实例对象调用。同样可以参照类方法不能被对象调用来理解。
3.原型方法,原型方法可以被实例直接访问或者People这个函数对象通过property来访问。
原型链的应用
基础方法
W3C不推荐直接使用系统成员__proto__,可以使用原型,但是尽量不要直接使用隐式原型。
Object.getPrototypeOf(对象)
获取对象的隐式原型,相当于 object.proto 一样的功能。上面是一个静态方法,静态方法意味着是不能用 obj.getPrototypeOf 调用,而是要用Object去调用。
Object.prototype.isPrototypeOf(对象)
判断当前对象(this)是否在指定对象的原型链上
A.prototype.isPrototypeOf(B)表达的意思是A对象的原型是不是在B的原型链上,A.property 代表的是A的原型, isPrototypeOf代表的是不是在B的原型链上。
对象 instanceof 函数
判断函数的原型是否在对象的原型链上 通俗的举个例子就是
b instanceof Array
上面判断的b是不是一个Array的实例,上面那句话 判断函数的原型是否在对象的原型链上 可以说成对象的原型链上是不是有函数对象的原型,如果有的话,就是Array这种类型。
Object.create(对象)
创建一个新对象,其隐式原型指向指定的对象 上面其实就是两个步骤:
- 创建一个对象
- 设置隐式原型指向这个对象(有点类似于设置isa指针)
所有的对象的原型链上都有Object的原型,或者说所有的对象都是Object类型,或者说请创建一个对象,不是Object类型的。都可以参照下面的代码
a的原型直接指向null,所以就是上面说的特殊情况。
Object.prototype.hasOwnProperty(属性名)
判断一个对象自身是否拥有某个属性,不是原型链上面有的
应用
类数组转换为真数组
Array.prototype.slice.call(类数组);
不用 [].slice.call(类数组);是因为他凭空要创建一个空的数组,而创建空的数组的目的是为了调用slice的方法,完全可以去array的原型去找到。
实现继承
默认情况下,所有构造函数的父类都是Object 自定义构造函数继承于Object,Object是自定义构造函数的父类,自定义构造函数是自定义构造函数的子类。
圣杯模式
上面1,2,3是完成继承的最核心工作,只要三个箭头完成就实现了继承。
son.prototype = Object.create(father.prototype);
- Object.create(father.prototype),创建一个新对象,其隐式原型指向father.prototype.这样第一步就完成了。
- 将上面的值赋值给son.prototype,第二步完成。
son.prototype.constructor = son;这句代码完成第三步。
标准圣杯模式写法
一种更好的圣杯模式写法
之所以说上面的代码更好,是因为son.prototype.uber 指向father这个构造函数,而不是father的原型,因为这样写,就让son对象拥有了father的创建函数,在创建的时候就可以直接使用this.uber(firstName,lastName,age);创建了。而不再需要绑定this了,是因为this.uber就直接拿到了father.其实这里的this.uber就相当于super关键字。
最通用最牛逼的一种圣杯模式写法
this.myPlugin.inherit = (function () {
var Temp = function () { }
return function (son, father) {
Temp.prototype = father.prototype;
son.prototype = new Temp();
son.prototype.constructor = son;
son.prototype.uber = father.prototype;
}
}());
解读: Temp 本来是一个普通的函数,Temp.prototype = father.prototype;一句话产生巨大的效果.
属性描述符
属性描述符的配置参考:developer.mozilla.org/zh-CN/docs/…
属性描述符:它表达了一个属性的相关信息(元数据),它本质上是一个对象。
- 数据属性
- 存取器属性
- 当给它赋值,会自动运行一个函数
- 当获取它的值时,会自动运行一个函数
最简单使用
function User(name, age) {
this.name = name;
//年龄的取值范围是 0 - 100
//如果年龄的值小于了0,则赋值为0,如果年龄的值大于了100,则赋值为100
var _age;
Object.defineProperty(this, "age", {
get: function() {
return _age;
},
set: function(val) {
if (val < 0) {
val = 0;
} else if (val > 100) {
val = 100;
}
_age = val;
}
})
this.age = age;
}
var u = new User("abc", -1);
u.age = u.age + 10000;
console.log(u.age);
其他使用方法
var config = {
_x: 0,
_y: 0,
xDis: 2,
yDis: 2,
duration: 16,
width: 100,
height: 100
}
Object.defineProperty(config, "x", {
get: function() {
return this._x;
},
set: function(val) {
if (val < 0) {
val = 0;
} else if (val > document.documentElement.clientWidth - this.width) {
val = document.documentElement.clientWidth - this.width;
}
this._x = val;
div.style.left = val + "px";
}
})
Object.defineProperty 的其他使用
var obj = {
x: 1,
y: 2
};
Object.defineProperty(obj, "name", {
value: "abc",
writable: false,
enumerable: true //不可迭代 遍历
})
其他的属性描述符
Object.getOwnPropertyDescriptor
获取某个对象的某个属性的属性描述符对象(该属性必须直接属于该对象)
执行上下文
函数执行上下文:一个函数运行之前,创建的一块内存空间,空间中包含有该函数执行所需要的数据,为该函数执行提供支持。
执行上下文栈:call stack,所有执行上下文组成的内存空间。
栈:一种数据结构,先进后出,后进先出。
全局执行上下文:所有JS代码执行之前,都必须有该环境。
JS引擎始终执行的是栈顶的上下文。
执行上下文中第一件事是要先确定this的指向
执行上下文中的内容
- this指向
1). 直接调用函数,this指向全局对象 2). 在函数外,this指向全局对象 3). 通过对象调用或new一个函数,this指向调用的对象或新对象
- VO 变量对象
Variable Object:VO 中记录了该环境中所有声明的参数、变量和函数
Global Object: GO,全局执行上下文中的VO
Active Object:AO,当前正在执行的上下文中的VO
1). 确定所有形参值以及特殊变量arguments 声明和形参名称一样的变量,并且给变量赋值为实参的值
2). 确定函数中通过var声明的变量,将它们的值设置为undefined,如果VO中已有该名称,则直接忽略。 如果参数已经有了这个声明的变量,这个已经有的变量可能是在形参声明的,也可能是函数中(声明var同名变量的位置比当前的位置靠前)通过var声明的,这里说的忽略是指的是忽略再声明的过程和值设为undefined的过程
3). 确定函数中通过字面量声明的函数,将它们的值设置为指向函数对象,如果VO中已存在该名称,则覆盖。
a.这里的函数必须是字面量声明的函数。什么是字面量声明的函数呢?
比方说 function A() { }
但 var A = function () { }
就不是字面量声明的函数。
b.如果前面的形参和var变量还没声明到和函数名称相同的变量,则这里会直接声明一个函数的变量。比方说 A:function A() { }
如果遇到前面已经有了和函数名称一样的变量,这里包括前面的参数和var变量声明的,都会直接把这个变量的值改为 指向函数的地址。为什么函数高级一些呢,因为函数是一等公民。
当一个上下文中的代码执行的时候,如果上下文中不存在某个属性,则会从之前的上下文寻找。
下面分析几个题目
第一题
<script>
var g1 = 123;
function A(a, b) {
console.log(a, b, g1);
var b = 123;
function b() {}
var a = function() {}
}
var g2 = 456;
var g3 = function() {}
A(1, 2);
</script>

下面详细分析一下: 首先分析全局的GO的情况,里面有g1,g2,g3,还有一个函数的字面量声明,再添加一个fn变量。
继续往下看,到了A(1,2) 这一句,于是产生了新的调用过程,创建一个新的VO,也就是上图的AO,继续分析AO中有什么?先是第一步创建形参的两个变量 a:1 ,b:2.
继续看函数A中var声明的变量,函数中有 var b var a这两个变量声明,本来的步骤应该是继续声明 a = undefined b= undefined ,根据上面的第二条规则,变量名和形参名重名,则忽略这个过程。
下面开始第三个过程,看函数的字面量声明,这里可以看到有function b {},虽然和前面声明的变量有冲突,但是由于函数是一等公民,这里将b:fn(函数b的地址)
这就是是上图结果的原因。
下面开始看执行阶段,先是看 console.log(a, b, g1); 此时 a:1 b:fn, g1 从自己的找不到就往上找,可以从前一个VO中找到g1 = 123,
接着往下看 var b = 123;这一句忽略定义,只看赋值,此时将 b:fn 变成了 b:123,
继续往下看 function b() {} 这一句完全是定义,不管,继续往下看。
继续往下看 var a = function() {},忽略定义,只看赋值,这里将 a赋值为fn
最后一句赋值,就是 console.log(a, b); a为fn,b为123.
第二题
var foo = 1;
function bar() {
console.log(foo); //undefined
if (!foo) {
var foo = 10;
}
console.log(foo); //10
}
bar();
***只要不是函数里面套函数的中声明var 变量,在考虑当前VO的时候也要考虑,也需要按照上面的方案二的规则去执行 ***
分析: 全局上下文GO: foo : undefined---> foo----> 1 bar :fn
bar的VO: foo :undefined 执行了
if (!foo) {
var foo = 10;
}
后foo:10
第三题
在考虑第三条的时候,去确定vo的时候不要去考虑函数的实际执行,具体这个题目来说就是虽然return 永远不会执行,但是考虑a函数的时候仍然要按照第三条规则去执行。
var a = 1;
function b() {
console.log(a); // fn
a = 10;
return;
function a() { }
}
b();
console.log(a); //1
第四题
严格按照确定 vo 和代码执行两个阶段去考虑就没有问题
console.log(foo); //fn C
var foo = "A";
console.log(foo) //A
var foo = function () {
console.log("B");
}
console.log(foo); //fn B
foo(); // B
function foo(){
console.log("C");
}
console.log(foo) //fn B
foo(); // B
第五题
在当前VO确定完后,所有形参和变量的值都是从当前VO中去取的而不涉及到原来的形参等东西
var foo = 1;
function bar(a) {
var a1 = a;
var a = foo;
function a() {
console.log(a); //1
}
a1();
}
bar(3);
GO: foo:1 bar:fn
AO:
a = 3;----> a:fn ---------------> a = 1
a1 = undefined; ---> a1 = fn
下面开始执行代码
执行 var a1 = a; 这句的意思是 将 a1 等于当前 a的值,而不是说 a1 = 等于形参 a1 的值,
继续下面的代码 a = 1;
最后执行输出a的值为1.
作用域链
- VO中包含一个额外的属性,该属性指向创建该VO的函数本身
- 每个函数在创建时,会有一个隐藏属性
[[scope]],它指向创建该函数时的AO - 当访问一个变量时,会先查找自身VO中是否存在,如果不存在,则依次查找
[[scope]]属性。
某些浏览器会优化作用域链,函数的[[scope]]中仅保留需要用到的数据。
题目1
var g = 0;
function A() {
var a = 1;
function B() {
var b = 2;
var C = function() {
var c = 3;
console.log(c, b, a, g);
}
C();
}
B();
}
A();

题目二 闭包的经典实现原理
var count = 100;
function A() {
var count = 0;
return function() {
count++;
console.log(count);
}
}
var test = A();
test();
test();
test();
console.log(count);

下图是闭包的经典实现原理图
其实就是在test=A();这句代码执行完后function A的VO就应该销毁了,但是由于全局环境的test能找到返回的匿名函数,而匿名函数的scope又指向这个vo,这样count本来在A声明中的变量就保存下来了,这样就可以使用test()来使用count,这就是闭包的经典实现原理。
题目三
var a = 1;
function A() {
console.log(a);
}
function Special() {
var a = 5;
var B = A;
B();
}
Special();

题目四
var foo = { n: 1 };
(function (foo) {
console.log(foo.n); // 1
foo.n = 3;
var foo = { n: 2 };
console.log(foo.n); // 2
})(foo);
console.log(foo.n); // 3

题目五
var food = "rice";
var eat = function () {
console.log(`eat ${food}`);
};
(function () {
var food = "noodle";
eat();//eat rice
})();

题目六
function A() {
for (var i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i);
}, 1000)
}
}
A();
console.log(i);


题目七
for (var i = 0; i < 3; i++) {
(function (i) {
setTimeout(function () {
console.log(i);
}, 1000)
}(i));
}

总结:
1.函数可以使用外边的东西
2.外边的东西是以当时声明的时间的变量值为准,而不是以运行和调用的时候为准。
事件循环
异步:某些函数不会立即执行,需要等到某个时机成熟后才会执行,该函数叫做异步函数。
浏览器的线程:
- JS执行引擎:负责执行JS代码,只有它能执行js代码
- 渲染线程:负责渲染页面
- 计时器线程:负责计时
- 事件监听线程:负责监听事件
- http网络线程:负责网络通信
异步函数包括:用户的操作(就是事件监听),计时,ajax(网络通信);
事件队列:一块内存空间,用于存放执行时机到达的异步函数。当JS引擎空闲(执行栈没有可执行的上下文),它会从事件队列中拿出第一个函数执行。
只有JS引擎能够执行JS 代码。
只有js引擎空闲才回去看执行队列。
js引擎里面有正在执行的代码,一定要把当前代码的执行完才会去事件队列里面看一下,哪些代码等着被执行。
事件循环:event loop,是指函数在执行栈、宿主线程、事件队列中的循环移动。
执行栈空的时候拿事件队列去执行,执行栈执行过程可能会像宿主环境发起一个异步的函数,当异步函数条件满足又要把他放到事件队列里面。执行事件队列的函数的时候又到执行栈。
对象混合
/**
* obj2混合到obj1产生新的对象
*/
this.myPlugin.mixin = function (obj1, obj2) {
return Object.assign({}, obj1, obj2);
// var newObj = {};
// //复制obj2的属性
// for (var prop in obj2) {
// newObj[prop] = obj2[prop];
// }
// //找到obj1中有但是obj2中没有的属性
// for (var prop in obj1) {
// if (!(prop in obj2)) {
// newObj[prop] = obj1[prop];
// }
// }
// return newObj;
}
对象混合的一个常用场景是函数的参数,如果函数的参数如果没有传的话,我想给他一个默认值,这种情况我就可以使用对象混合了。
对象克隆
// 分别分为对象,数组去处理,处理到原始类型为止。
/**
* 克隆一个对象
* @param {boolean} deep 是否深度克隆
*/
this.myPlugin.clone = function (obj, deep) {
if (Array.isArray(obj)) {
if (deep) {
//深度克隆
var newArr = [];
for (var i = 0; i < obj.length; i++) {
newArr.push(this.clone(obj[i], deep));
}
return newArr;
}
else {
return obj.slice(); //复制数组
}
}
else if (typeof obj === "object") {
var newObj = {};
for (var prop in obj) {
if (deep) {
//深度克隆
newObj[prop] = this.clone(obj[prop], deep);
}
else {
newObj[prop] = obj[prop];
}
}
return newObj;
}
else {
//函数、原始类型
return obj; //递归的终止条件
}
}
防抖
函数防抖可以想象到电梯的场景,只有有人进去,电梯门就要等一段时间才能关闭,进一个人要等这个时间,进去一个人,时间都要重新开始计算。
this.myPlugin.debounce = function (callback, time) {
var timer;
return function () {
clearTimeout(timer);//清除之前的计时
var args = arguments; //利用闭包保存参数数组
timer = setTimeout(function () {
callback.apply(null, args);
}, time);
}
}
在函数内部返回一个函数叫做高阶函数。
首先上面方法执行后返回了一个新的函数。可以理解为this.myPlugin.debounce 其实就是下面的代码,这时间如果你执行 his.myPlugin.debounce(width)其实就相当于下面的匿名函数执行 function (width),由于setTimeout里面也是函数,所以这里为了拿到真正传进去的参数,需要执行用args这个保存起来,再去执行。
function () {
clearTimeout(timer);//清除之前的计时
var args = arguments; //利用闭包保存参数数组
timer = setTimeout(function () {
callback.apply(null, args);
}, time);
}
}
有时也会看到下面的写法,其实下面的用了一个ES6的一个语法,把函数的参数收集到args的数组中去。
export default function debounce(fn, duration = 100) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, duration);
};
}
节流
this.myPlugin.throttle = function (callback, time, immediately) {
if (immediately === undefined) {
immediately = true;
}
if (immediately) {
var t;
return function () {
if (immediately) {
if (!t || Date.now() - t >= time) { //之前没有计时 或 距离上次执行的时间已超过规定的值
callback.apply(null, arguments);
t = Date.now(); //得到的当前时间戳
}
}
}
}
else {
var timer;
return function () {
if (timer) {
return;
}
var args = arguments; //利用闭包保存参数数组
timer = setTimeout(function () {
callback.apply(null, args);
timer = null;
}, time);
}
}
}
防抖和节流的区别
1、防抖只执行最后一次操作。
2、节流是在某一段时间内只执行一次操作,这个操作往往是第一次。
防抖和节流辅助记忆
防抖可以想象电梯的案例,只有没有人进来之后,间隔两秒才能电梯门才能关上,而一直有人进来,电梯门永远关不上。
节流可以想象一个火箭发射按钮,而炮弹只能隔两秒才能发射一发,所以你第一次按了火箭发射按钮后,再不停的去按发射按钮,都不会发射。只有过了这2s的时间后按发射按钮才会再发射。
防抖和节流 典型使用场景
防抖
典型场景:搜索框搜索输入
节流
典型场景:高频事件 快速点击、鼠标滑动、resize 事件、scroll。