原型和原型链
- 原型:每个函数都有一个prototype属性,这个属性指向调用该构造函数而创建的实例的原型。__proto__是每个对象都会有的属性,这个属性指向该对象的原型。每个原型都有一个constructor属性,指向该原型关联的构造函数。
- 原型链:当我们访问对象的一个属性或方法时,会先在对象自身中寻找,如果没有找到则会去原型中寻找,如果在原型中还没有找到,则会去原型的原型中寻找,直到找到Object.prototype,Object.prototype没有原型,如果在Object.prototype中依然没有找到,则返回undefined。这样一条向上查找的路径就是原型链。
作用域和作用域链
- 作用域:
- 定义:作⽤域就是变量与函数的可访问范围,即作⽤域控制着变量与函数的可⻅性和⽣命周期。
- 分类:
- 全局作用域
- 函数作用域
- 块级作用域
- 拓展(了解即可):
- 词法作用域(lexical scoping,也叫静态作用域):函数的作用域在函数定义的时候就决定了,JavaScript采用的就是词法作用域。
- 动态作用域:函数的作用域是在函数调用的时候才决定的,bash就是动态作用域
- 作用域链:
- 当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会去父级执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
- 作⽤域链的作⽤是保证执⾏环境⾥有权访问的变量和函数是有序的,作⽤域链的变量只能向上访问,变量访问到 window 对象即被终⽌,作⽤域链向下访问变量是不被允许的。
闭包
- 闭包指的是引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的
- 用途:读取函数内部的变量并让这些变量始终保存在内存中
- 优点:1.可以避免全局变量的污染;2.可以缓存变量
- 缺点:因为闭包会保留它们包含函数的作用域,所以比其他函数更占用内存。过度使用闭包可能导致内存过度占用,甚至造成内存泄漏
- 使用闭包的注意点:在退出函数之前,将不使用的局部变量全部删除
模拟实现call和apply
call()方法使用一个指定的this值和单独给出的一个或多个参数来调用一个函数。apply()方法调用一个具有给定this值的函数,以及以一个数组(或一个类数组对象)的形式提供的参数。call()方法的语法和作用与apply()方法类似,只有一个区别,就是call()方法接受的是一个参数列表,而apply()方法接受的是一个包含多个参数的数组。
Function.prototype.call2 = function (context) {
context = context || window;
context.fn = this;
var args = Array.prototype.slice.call(arguments, 1);
// var args = [];
// for (var i = 1, len = arguments.length; i < len; i++) {
// args.push("arguments[" + i + "]");
// }
var res = context.fn(...args);
// var res = eval("context.fn(" + args + ")");
delete context.fn;
return res;
};
Function.prototype.apply2 = function (context, arr) {
context = context || window;
context.fn = this;
var result;
if (!arr) {
result = context.fn();
} else {
result = context.fn(...arr);
// var args = [];
// for (var i = 0, len = arr.length; i < len; i++) {
// args.push("arr[" + i + "]");
// }
// result = eval("context.fn(" + args + ")");
}
delete context.fn;
return result;
};
模拟实现bind
bind()方法创建一个新的函数,在bind()被调用时,这个新函数的this被指定为bind()的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
Function.prototype.bind2 = function (context) {
var self = this;
// 获取bind2函数从第二个参数到最后一个参数
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {};
var fBound = function () {
// 这个时候的arguments是指bind返回的函数传入的参数
var bindArgs = Array.prototype.slice.call(arguments);
// 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
// 以上面的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改成 this ,实例会具有 habit 属性
// 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
return self.apply(
this instanceof fBound ? this : context,
args.concat(bindArgs)
);
};
// 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
fNOP.prototype = this.prototype;
// 通过一个空函数来进行中转
fBound.prototype = new fNOP();
return fBound;
}
简单写法:
Function.prototype.bind2 = function (context, ...args) {
var self = this;
var fBound = function () {
return self.apply(
this instanceof self ? this : context,
args.concat(...arguments)
);
};
fBound.prototype = Object.create(this.prototype);
return fBound;
};
模拟实现new
function objectFactory(constructor, ...args) {
var obj = Object.create(constructor.prototype);
var res = constructor.apply(obj, args);
return typeof res === "object" ? res : obj;
}
类数组对象
- 什么是类数组对象?
- 拥有一个 length 属性和若干索引属性
- 不具有数组所具有的的方法
- 常见的类数组对象:
- Arguments 对象
- 一些 DOM 方法的返回值(例如document.getElementsByTagName()等)
- 类数组转数组:
var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }
// 1. slice
Array.prototype.slice.call(arrayLike); // ["name", "age", "sex"]
// 2. splice
Array.prototype.splice.call(arrayLike, 0); // ["name", "age", "sex"]
// 3. ES6 Array.from
Array.from(arrayLike); // ["name", "age", "sex"]
// 4. apply
Array.prototype.concat.apply([], arrayLike)
创建对象的多种方式及优缺点
1. 工厂模式
function createPerson(name) {
let o = new Object();
o.name = name;
o.getName = function () {
console.log(this.name);
};
return o;
}
let person1 = createPerson("jjc");
- 缺点:对象无法识别,因为所有的实例都指向同一个原型
2. 构造函数模式
function Person(name) {
this.name = name;
this.getName = function () {
console.log(this.name);
};
}
let person1 = new Person("jjc");
- 优点:实例可以识别为一个特定的类型
- 缺点:每次创建实例时,每个方法都要被创建一次
构造函数模式优化
function Person(name) {
this.name = name;
this.getName = getName;
}
function getName() {
console.log(this.name);
}
let person1 = new Person("jjc");
- 优点:解决了相同逻辑的函数重复定义的问题
- 缺点:全局作用域被搞乱了
3. 原型模式
function Person() {}
Person.prototype.name = "jjc";
Person.prototype.getName = function () {
console.log(this.name);
};
let person1 = new Person();
- 优点:方法不会重新创建
- 缺点:1. 所有的属性和方法都共享 2. 不能初始化参数
继承的多种方式及优缺点
1. 原型链继承
function Parent() {
this.name = "jjc";
}
Parent.prototype.getName = function () {
console.log(this.name);
};
function Child() {}
Child.prototype = new Parent();
let child1 = new Child();
child1.getName();
- 缺点:
-
- 引用类型的属性被所有实例共享,举个例子:
function Parent() { this.names = ["kevin", "daisy"]; } function Child() {} Child.prototype = new Parent(); let child1 = new Child(); child1.names.push("yasuo"); console.log(child1.names); // ["kevin", "daisy", "yasuo"] let child2 = new Child(); console.log(child2.names); // ["kevin", "daisy", "yasuo"] -
- 在创建 Child 的实例时,不能向 Parent 传参
-
2. 借用构造函数(经典继承)
function Parent() {
this.names = ["kevin", "daisy"];
}
function Child() {
Parent.call(this);
}
let child1 = new Child();
child1.names.push("yayu");
console.log(child1.names); // ["kevin", "daisy", "yayu"]
let child2 = new Child();
console.log(child2.names); // ["kevin", "daisy"]
- 优点:
-
- 避免了引用类型的属性被所有实例共享
-
- 可以在 Child 中向 Parent 传参,举个例子:
function Parent(name) { this.name = name; } function Child(name) { Parent.call(this, name); } let child1 = new Child("kevin"); console.log(child1.name); // kevin let child2 = new Child("daisy"); console.log(child2.name); // daisy
-
- 缺点:方法都在构造函数中定义,每次创建实例都会创建一遍方法
3. 组合继承
原型链继承和经典继承双剑合璧
function Parent(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
Parent.prototype.getName = function () {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
var child1 = new Child("kevin", "18");
child1.colors.push("black");
child1.getName(); // kevin
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]
var child2 = new Child("daisy", "20");
child2.getName(); // daisy
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]
- 优点:融合了原型链继承和借用构造函数继承的优点,是 JavaScript 中最常用的继承模式
4. 原型式继承
function createObj(o) {
function F(){}
F.prototype = o;
return new F();
}
就是 Object.create 的模拟实现,将传入的对象作为创建的对象的原型
- 缺点:包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样
let person = {
name: "kevin",
friends: ["daisy", "kelly"],
};
let person1 = createObj(person);
let person2 = createObj(person);
person1.name = "person1";
console.log(person2.name); // kevin
person1.firends.push("taylor");
console.log(person2.friends); // ["daisy", "kelly", "taylor"]
注意:修改person1.name的值,person2.name的值并未发生改变,并不是因为person1和person2有独立的 name 值,而是因为person1.name = 'person1',给person1添加了 name 值,并非修改了原型上的 name 值
5. 寄生式继承
创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象
function createObj(o) {
let clone = object.create(o);
clone.sayName = function () {
console.log("hi");
};
return clone;
}
缺点:跟借用构造函数模式一样,每次创建对象都会创建一遍方法
6. 寄生式组合继承
为了方便阅读,在这里贴一下组合继承的代码
function Parent(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
Parent.prototype.getName = function () {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
let child1 = new Child("kevin", "18");
console.log(child1);
组合继承最大的缺点是会调用两次父构造函数,一次是设置子类型实例的原型的时候:
Child.prototype = new Parent();
一次在创建子类型实例的时候:
let child1 = new Child('kevin', '18');
那么我们该如何精益求精,避免这一次重复调用呢?
如果我们不使用 Child.prototype = new Parent() ,而是间接的让 Child.prototype 访问到 Parent.prototype 呢?
function Parent(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
Parent.prototype.getName = function () {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
function inheritPrototype(subType, superType) {
let prototype = Object.create(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 赋值对象
}
inheritPrototype(Child, Parent);
let child1 = new Child("kevin", "18");
console.log(child1);
这里只调用了一次 Parent 构造函数,避免了 Child.prototype 上不必要也用不到的属性。而且,原型链仍然保持不变,因此 instanceof 操作符和 isPrototypeOf() 方法正常有效。寄生式组合继承可以算是引用类型继承的最佳模式
参考资料:《JavaScript 深入系列》juejin.cn/column/7035…