构造函数
构造函数 ,是一种特殊的方法。主要用来在创建对象时初始化对象,即为对象成员变量赋初始值,总与 new 运算符一起使用在创建对象的语句中。没有返回值,不能被直接调用,必须通过 new 运算符在创建对象时才会自动调用。
class
ES6 引入了 Class(类)这个概念,作为对象的模板。通过 class 关键字,可以定义类。Class 不存在变量提升(hoist)。
constructor 方法是类的默认方法,通过 new 命令生成对象实例时,自动调用该方法。一个类必须有 constructor 方法,如果没有显式定义,一个空的 constructor 方法会被默认添加。
constructor 方法默认返回实例对象(即 this),类的方法内部如果含有 this,它默认指向类的实例。
如果一个子类通过extends
关键字继承了父类,那么在子类的constructor
构造函数中必须优先调用一下super
super 作为函数调用时,代表父类的构造函数。子类必须在 constructor 方法中调用 super 方法,用来新建父类的 this 对象。super 虽然代表了父类 A 的构造函数,但是返回的是子类 B 的实例,即 super 内部的 this 指的是 B,因此
super()
在这里相当于A.prototype.constructor.call(this)
。
super 作为对象时,指向父类的原型对象。定义在父类实例上的方法或属性,是无法通过 super 调用的。super.print()
实际上执行的是super.print.call(this)
。
使用 super 的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。console.log(super); // 报错
ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先创造父类的实例对象 this(所以必须先调用 super 方法),然后再用子类的构造函数修改 this。
大多数浏览器的 ES5 实现之中,每一个对象都有
__proto__
属性,指向对应的构造函数的prototype
属性。Class 作为构造函数的语法糖,同时有prototype
属性和__proto__
属性,因此同时存在两条继承链。
(1)作为一个对象,子类的__proto__
属性,表示构造函数的继承,总是指向父类。
(2)作为一个构造函数,子类的原型prototype
属性的__proto__
属性,表示方法的继承,总是指向父类的实例prototype
属性。
public:允许在类的内外被调用
private:只允许在类的内部被调用
protected:允许在类的内部及继承的子类中调用
static:静态方法/属性。需要直接通过类名来调用,不能通过 new 操作符生成的实例对象进行调用。故转换成 es5 写法时,应直接绑定在构造函数上,而不是绑定在 prototype 上。Dog.sayName = function() { console.log('123') }
抽象类: abstract 修饰, 里面可以没有抽象方法。但有抽象方法(abstract method
)的类必须声明为抽象类(abstract class
),抽象方法 ,可以不包含具体实现,但是要求子类中必须实现此方法。
实现一个 new
function _new(fn, ...arg) {
// Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
const obj = Object.create(fn.prototype);
const ret = fn.apply(obj, arg);
return ret instanceof Object ? ret : obj;
}
// 实现一个new
var Dog = function(name) {
this.name = name
}
Dog.prototype.sayName = function() {
console.log('my name is ' + this.name)
}
let sanmao = new Dog('三毛')
let simao = _new(Dog, '四毛')
sanmao.sayName()
simao.sayName()
new 做了什么?
- 创建一个空对象
obj
:var obj = {}
- 将
obj
的__proto__
成员指向了Base
函数对象prototype
成员对象:obj.__proto__ = Base.prototype
- 将
Base
函数对象的this
指针替换成obj
,再调用Base
函数:Base.call(obj)
原型链(构造函数创建对象的完整的原型链7个框,12条线)
function Foo() {
Foo.a = function () {
console.log(1)
}
this.a = function () {
console.log(2)
}
}
Foo.prototype.a = function () {
console.log(3)
}
Foo.a = function () {
console.log(4)
}
Foo.a(); // 4
let obj = new Foo();
obj.a(); // 2:属性上存在,则不会去原型链上查找
Foo.a(); // 1:Foo.a 被重新赋值了
已知一个构造函数,该构造函数的实例,可以通过
__proto__
属性访问到原型中的属性和方法,后来发现原型之上还有原型,依次类推,包括数组、正则、函数等等所有的对象类型都可以通过查找一层层的__proto__
属性都最终找到了某个对象(原型链的顶端),我们把这个查找过程称之为这个对象的原型链。凡是通过
new Function()
创建的对象都是函数对象,其他的都是普通对象。普通对象没有prototype
,但有__proto__
属性。对象有属性
__proto__
,指向该对象的构造函数的原型对象。万物皆对象。方法除了有属性
__proto__
,还有属性prototype
,prototype
指向该方法的原型对象。-
对象的原型链
- 结论1:
- 对象字面量是
Object
构造函数的实例 - 数组是
Array
构造函数的实例 - 正则表达式是
RegExp
构造函数的实例
- 对象字面量是
- 结论2:
-
Object.prototype.__proto__===null
(原型链最顶端) - 自定义构造函数的默认的原型对象的
__proto__
指向Object.prototype
-
Array、RegExp、String、Number、Boolean
这些函数的原型对象的__proto__
都指向Object.prototype
-
- 结论1:
-
函数的原型链
- 结论1:所有的函数都是
Function
的实例- 推论1:
函数.__proto__===Function.prototype
- 推论2:
Array.__proto__===Function.prototype
- 推论3:
RegExp.__proto__===Function.prototype
- 推论4:
Object.__proto__===Function.prototype
- 推论5:
Function.__proto__===Function.prototype
- 推论1:
- 结论2:
Function.prototype.__proto__===Object.prototype
(原型链最顶端)
- 结论1:所有的函数都是
-
区分对象类型
console.log(Object.prototype.toString.call(123)) // [object Number] console.log(Object.prototype.toString.call('123')) // [object String] console.log(Object.prototype.toString.call(undefined)) // [object Undefined] console.log(Object.prototype.toString.call(true)) // [object Boolean] console.log(Object.prototype.toString.call({})) // [object Object] console.log(Object.prototype.toString.call([])) // [object Array] console.log(Object.prototype.toString.call(function(){})) // [object Function]
五种继承
-
扩展继承
function Fn() { } // 给构造函数的 prototype 对象添加属性和方法,从而使该构造函数的每个实例都能访问到 Fn.prototype.say = function () { }
-
替换继承
function Fn() { } // 重新设置了构造函数的 prototype 的值,让它指向一个全新的对象,从此以后的实例都只能访问到这个对象中的属性和方法 Fn.prototype = { constructor: Fn, say: function() { }, run: function() { } }
-
混入继承(拷贝继承)
实现功能:将一个对象中的属性和方法分别遍历添加到另一个对象中
实现方式:for...in... 循环对象的每一个属性function Person(obj) { // 将 obj 中的属性遍历添加到 this 中 for(var key in obj) { // key 是一个变量,该变量保存了每一次遍历 obj 获取到的属性的名称 this[key] = obj[key]; } } var p3=new Person({ name: "李四", age: 18, gender: "未知", grade: "小五", className: "5(3)班" });
-
原型式继承(经典继承)
a. 帮助用户创建一个新对象,让这个新对象可以访问到指定对象中的属性和方法
b.新对象.__proto__ === 指定对象
c. 原型式继承不需要关心构造函数,如果你要关心构造函数,那么就不要使用原型式继承
d. 替换继承是重新创建对象替换掉原来的原型,原型式继承是对象已经存在,将此对象赋给新对象,新对象的原型为原来存在的对象var obj = { a:10, b:20, c:30 }; // ES5 中实现了经典继承:Object.create() var o1 = Object.create(obj); console.log(o1.__proto__ === obj); // true console.log(o1.a); // 10 o1.a = 100; console.log(o1.a); // 100,给 o1 自己添加了属性,再也访问不到 obj 中的 a 属性
-
构造函数继承
a. 一个称为父类构造函数,一个称为子类构造函数
b. 如果父类构造函数中的代码完全适用于子类构造函数,就在子类构造函数中运用上下文模式借用父类构造函数,从而给子类的实例添加属性和方法function Animal(type) { this.type = type } Animal.prototype.run = function() { console.log('run') } function Cat(name, type) { // 此处采用上下文调用,使 this 指向 Cat 的实例 // 故 Cat 的实例可以继承 Animal 的实例上的属性和方法 Animal.call(this, type) this.name = name } // 让 Cat 继承 Animal 原型链上的方法 Cat.__proto__ = Animal.prototype Cat.prototype.say = function() { console.log('say') } console.log(new Cat('abc', 'cat'))
this 指向分析
- 函数调用:this 指向 window,返回值由 return 语句决定。若指定了严格模式('use strict'),this 指向 undefined。
function f1() {};
f1();
- 方法调用:this 指向调用者,返回值由 return 语句决定
const obj = {
name: "张三",
say: function() {}
};
obj.say();
const obj1 = {
num: 10,
hello: function() {
console.log(this); // obj1
setTimeout(function() {
console.log(this); // window,匿名函数没有直接调用者,this 指向 window
});
setTimeout(() => {
console.log(this); // obj1,箭头函数,this 指向最近的函数的 this 指向
});
}
}
obj1.hello();
- 构造函数调用
function Fn(name) {
this.name = name;
}
var f1 = new Fn('zhangsan');
首先 new 关键字会创建一个空的对象,然后会自动调用一个函数 apply 方法,将 this 指向这个空对象,这样的话,函数内部的 this 就会被这个空的对象替代。
- 若无 return 语句,则默认返回 this 即构造函数的实例。
- 若有 return 语句,如果 return 了一个基本数据类型,则最终返回 this
- 若有 return 语句,如果 return 了一个对象,则最终返回这个对象
- 上下文调用
foo.call();
foo.apply();
- this 由第一个参数决定,返回值由 return 语句决定
- 第一种情况:实参为 null 或 undefined,函数内部的 this 指向 window
- 第二种情况:实参为 Number、String、Boolean,函数内部的 this 指向对应的基本包装类型的对象
- 第三种情况:实参为对象数据,函数内部的 this 指向该对象
- call 和 apply 的不同:call 方法的第一个实参表示 this 的指向,后面依次表示 foo 函数传递的参数,以逗号隔开;apply 方法的第一个实参表示 this 的指向,第二个参数为数组,表示 foo 函数传递的实参。
- bind()
bind 也可以有多个参数,参数还可以在执行的时候再次添加。但是要注意的是,参数是按照形参的顺序进行的。bind 方法返回的是一个修改过后的函数,需要调用。
call 和 apply 都是改变上下文中的 this 并立即执行这个函数,bind 方法可以让对应的函数想什么时候调就什么时候调用,并且可以将参数在执行的时候添加。
var a = {
user: "追梦子",
fn: function(e, d, f) {
console.log(this.user); // 追梦子
console.log(e, d, f); // 10 1 2
}
}
var b = a.fn;
var c = b.bind(a, 10);
c(1, 2);
- 箭头函数:this 指向上下文函数 this 的指向
const obj = {
radius: 10,
diameter() {
return this.radius * 2 // 普通函数,this 指向直接调用它的对象 obj
},
perimeter: () => 2 * Math.PI * this.radius // 箭头函数,this 指向上下文函数 this 的指向,这里上下文没有函数对象,就默认为 window
}
console.log(obj.diameter()) // 20
console.log(obj.perimeter()) // NaN
箭头函数和普通函数区别
- 箭头函数是匿名函数
- 箭头函数不能绑定 arguments,取而代之用 rest 参数
...
解决 - 箭头函数没有原型属性,故不能作为构造函数,不能使用 new
- 箭头函数的 this 永远指向其上下文函数的 this,没有办改变其指向,普通函数的 this 指向调用它的对象
函数防抖(debounce)
防抖:当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次。如果设定的时间到来之前,又一次触发了事件,就重新开始延时。
思路:返回一个函数,每次触发事件时都取消之前的定时器
需要注意问题:this 指向、参数的传递、是否要立即调用一次
-
使用场景
- 监听 resize 或 scroll,执行一些业务处理逻辑
- 搜索输入框,在输入后 200 毫秒触发搜索
实现
function debounce(fn, wait, immediate) {
let timer = null;
// 返回一个函数
return function(...args) { // 匿名函数没有直接调用者,this 指向 window
// 每次触发事件时都取消之前的定时器
// timer 是分配一个随机数字 id,clearTimeout 后,timer 的变量指向数字 id 还在, 只是定时器停止了
clearTimeout(timer);
// 判断是否要立即执行一次
if(immediate && !timer) {
fn.apply(this, args);
}
// setTimeout 中使用箭头函数,就是让 this 指向上下文函数 this 的指向
timer = setTimeout(() => {
fn.apply(this, args)
}, wait)
}
}
函数节流(throttle)
函数节流:当持续触发事件时,保证一定时间段内只调用一次事件处理函数。
有两种思路实现:使用时间戳和定时器
场景:商品预览图的放大镜效果时,不必每次鼠标移动都计算位置。
- 使用时间戳
function throttle1(fn, wait) {
// 记录上一次执行的时间戳
let previous = 0;
return function(...args) {
// 当前的时间戳,然后减去之前的时间戳,大于设置的时间间隔,就执行函数,否则不执行
if(Date.now() - previous > wait) {
// 更新上一次的时间戳为当前时间戳
previous = Date.now();
fn.apply(this, args);
}
}
}
第一次事件肯定触发,最后一次不会触发(比如说监听 onmousemove,则鼠标停止移动时,立即停止触发事件)
- 使用定时器
function throttle(fn, wait) {
// 设置一个定时器
let timer = null;
return function(...args) {
// 判断如果定时器不存在就执行,存在则不执行
if(!timer) {
// 到达函数执行时间后,设置下一个定时器。确保函数有序触发
timer = setTimeout(() => {
// 把 timer 赋值为 null,是为了释放内存,方便 boolean 判断。定时器并未停止
timer = null;
// 执行函数
fn.apply(this, args)
}, wait)
}
}
}
// 第一次事件不会触发(fn是放在 setTimeout中执行的,所以第一次触发事件至少等待 wait 毫秒之后才执行),最后一次一定触发
- 定时器和时间戳结合
function throttle(fn, wait) {
// 记录上一次执行的时间戳
let previous = 0;
// 设置一个定时器
let timer = null;
return function(...args) {
// 当前的时间戳,然后减去之前的时间戳,大于设置的时间间隔
if(Date.now() - previous > wait) {
clearTimeout(timer);
timer = null
// 更新上一次的时间戳为当前时间戳
previous = Date.now();
fn.apply(this, args);
} else if(!timer) {
// 设置下一个定时器
timer = setTimeout(() => {
timer = null;
fn.apply(this, args)
}, wait)
}
}
}