前言
继续冲冲冲!保佑实习上岸!
1. 对this的理解
this指向最后一次调用这个方法的对象
四种调用模式
- 函数调用模式
当一个函数不是一个对象的属性时,直接作为函数调用时,this指向全局对象
- 方法调用模式
当一个函数是一个对象的方法调用时,this指向这个对象
- 构造器调用模式
当一个函数new一个实例时,函数执行前会创建一个新对象,this指向这个对象
- apply、call、bind调用模式
apply:接收两个参数,一个是this绑定的对象,一个是参数数组
call:第一个是this绑定对象,后面的其余参数是传入函数执行的参数
bind:通过传入一个对象,返回一个this绑定了传入对象的心函数,这个函数的this指向了使用new时被改变,其余情况不会变
这四种模式,使用构造器调用模式的优先级最高,然后是apply、call和bind调用模式,然后是方法调用模式,然后是函数调用模式。
2. 实现call、apply、bind函数
call 函数的实现步骤:
-
判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
-
判断传入上下文对象是否存在,如果不存在,则设置为 window 。
-
处理传入的参数,截取第一个参数后的所有参数。
-
将函数作为上下文对象的一个属性。
-
使用上下文对象来调用这个方法,并保存返回结果。
-
删除刚才新增的属性。
-
返回结果。
Function.prototype.myCall = function(context) {
// 判断调用对象
if (typeof this !== "function") {
console.error("type error");
}
// 获取参数
let args = [...arguments].slice(1),
result = null;
// 判断 context 是否传入,如果未传入则设置为 window
context = context || window;
// 将调用函数设为对象的方法
context.fn = this;
// 调用函数
result = context.fn(...args);
// 将属性删除
delete context.fn;
return result;
}
apply 函数的实现步骤:
-
判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
-
判断传入上下文对象是否存在,如果不存在,则设置为 window 。
-
将函数作为上下文对象的一个属性。
-
判断参数值是否传入
-
使用上下文对象来调用这个方法,并保存返回结果。
-
删除刚才新增的属性
-
返回结果
Function.prototype.myApply = function(context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error")
}
let result = null
// 判断 context 是否存在,如果未传入则为 window
context = context || window
// 将函数设为对象的方法
context.fn = this
// 调用方法
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
// 将属性删除
delete context.fn
return result
}
bind 函数的实现步骤:
-
判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
-
保存当前函数的引用,获取其余传入参数值。
-
创建一个函数返回
-
函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。
Function.prototype.myBind = function(context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error")
}
// 获取参数
var args = [...arguments].slice(1),
fn = this;
return function Fn() {
// 根据调用方式,传入不同绑定值
return fn.apply(
this instanceof Fn ? this : context,
args.concat(...arguments)
)
}
}
3. 箭头函数的this指向哪里
箭头函数没有自己的this,他的this时捕获所在上下文的this作为自己的this
由于没有属于自己的this,所以不会被new调用,这个this也不会被改变
可以⽤Babel理解⼀下箭头函数:
// ES6
const obj = {
getArrow() {
return () => {
console.log(this === obj);
}
}
}
转化后:
// ES5,由 Babel 转译
var obj = {
getArrow: function getArrow() {
var _this = this;
return function () {
console.log(_this === obj);
}
}
}
4. 箭头函数与普通函数的区别
-
比普通函数更加简洁
-
没有自己的this
-
继承来的this指向永远不会改变
-
call()、apply()、bind()等方法不能改变箭头函数中this的指向
-
不能作为构造函数使用
-
没有自己的aruguments
-
没有prototype
-
不能用作Generator函数,不能使用yield关键字
5. 判断数组的方式有哪些?
-
instanceof
-
Array.isArray()
-
原型链
-
Object.prototype.toString.call()
-
Array.prototype.isPrototypeOf()
6. 数组去重
双重for循环
function distinct(arr) {
for(let i = 0; i < arr.length; i++) {
for(let j = i+1; j < arr.length; j++) {
if(arr[i] == arr[j]) {
arr.splice(j, 1)
arr.length--
j--
}
}
}
return arr
}
Array.sort() + 冒泡
function distinct(array) {
var res = [];
var sortedArray = array.concat().sort();
var seen;
for (var i = 0, len = sortedArray.length; i < len; i++) {
// 如果是第一个元素或者相邻的元素不相同
if (!i || seen !== sortedArray[i]) {
res.push(sortedArray[i])
}
seen = sortedArray[i];
}
return res;
}
Set去重
function distinct(arr) {
return [..new Set(arr)]
}
Object 键值对去重
function distinct(array) {
var obj = {};
return array.filter(function(item, index, array){
return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
})
}
7. 类数组
类数组 (arguments) ,是传给函数参数的数组
类数组拥有和数组相似的方法,例如:length
类数组如何转数组
-
Array.prototype.slice.call(arguments)
-
Array.prototype.splice.call(arguments, 0)
-
Array.prototype.concat.apply( [], arguments)
-
Array.from(arguments)
8. JS继承
- 原型链继承
function Parent() {
this.name = 'parent'
this.play = [1, 2, 3]
}
function Child() {
this.type = 'child'
}
Child.prototype = new Parent()
console.log(new Child())
虽然我们把child继承了parent,但是存在潜在问题
let child1 = new Child()
let child2 = new Child()
child1.play.push(2)
改变child1的play属性,child2也发生了改变。
这是因为两个实例共用同一个原型对象,内存空间是共享的。
- 构造函数继承
借助 call 调用 parent 函数
function Parent() {
this.name = 'parent'
}
Parent.prototype.getName = function() {
console.log(this.name)
}
function Child() {
Parent.call(this)
this.type = 'child'
}
let cld = new Child()
console.log(cld)
父类中的属性都继承到了child上,但是Parent原型链中的属性和方法无法继承
只能继承父类的实例属性,不能继承原型属性和方法
- 组合继承
function Parent() {
this.name = 'parent'
this.play = [1, 2, 3]
}
Parent.prototype.getName = function() {
console.log(this.name)
}
function Child() {
Parent.call(this)
this.type = 'child'
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
let cld = new Child()
console.log(cld)
结合前面两种继承方式,虽然之前遇到的问题解决了,但是每创建一个Child实例都要重新new一个Parent实例,造成多构造一次的性能消耗
- 原型式继承
let parent = {
name: 'parent',
play: [1, 2, 3],
gerName: function() {
return this.name
}
}
let person = Object.create(parent)
这种方式也存在缺点,因为Object.create 方法实现的是浅拷贝,多个实例的引用类型指向相同的内存,存在数据污染的可能
- 寄生式继承
let parent = {
name: 'parent',
play: [1, 2, 3],
getName: function() {
return this.name
}
}
function extend(obj) {
let clone = Object.create(obj)
clone.getPlay = function() {
return this.play
}
return clone
}
let person = extend(parent)
缺点和上面讲的原型式继承一样
- 寄生组合式继承
function extend(parent, child) {
child.prototype = Object.create(parent.prototype)
child.prototype.constructor = child
}
function Parent() {
this.name = 'parent',
this.play = [1, 2, 3]
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
Parent.call(this)
this.type = 'child'
}
extend(Parent, Child)
Child.prototype.getPlay = function() {
return this.type
}
let child = new Child()
console.log(child);
这是所有继承方式里面相对最优的继承方式
ES6中的extends实际采用的也是寄生组合继承方式