19对象和原型

1.apply、call 、bind有什么作用,什么区别?

apply和call

apply和call都是为了改变某个函数运行时的上下文而存在的(就是为了改变函数内部this的指向);

如果使用apply或call方法,那么this指向他们的第一个参数,apply的第二个参数是一个参数数组,call的第二个及其以后的参数都是数组里的元素,需要全部列举出来。apply 和 call 的用法几乎相同, 唯一的差别在于参数传递方式不一样。 apply粗鲁些直接一个数组怼过来,而call则细腻些将数组里的元素一个一个传递进来

function(){console.log(this.a)}.call(undefined)

如果你传的 context 是 null 或者 undefined,那么 window 对象就是默认的 context(严格模式下默认 context 是 undefined)

他们常用用法:

  • 求数组中的最大和最小值
    利用他们扩充作用域 从而借助Math来使用min和max的方法
    注:由于没有对象调用这个方法,所以第一个参数可以写作null或者Math本身
    1
    2
    3
    var arr = [10,20,30,40,50]
    var max = Math.max.apply(null,arr)
    var min = Math.min.call(Math,10,20,30,40,50)

(ps:这样看起来apply和call更像是寄生虫,让它的参数 能够使用本不属于它的技能=.=)

  • 数组之间的追加
    1
    2
    3
    4
    5
    var arr1 = [1,2,3]
    var arr2 = [4,5,6]
    var total = [].push.apply(arr1,arr2) //返回长度6因为this是arr1,所以追加到arr1上
    //arr1 [1,2,3,4,5,6]
    //arr2 [4,5,6]

image.png

  • 验证是否是数组(前提toString()方法没有被重写)
    1
    2
    3
    4
    5
    function isArray(obj){
    return Object.prototype.toString.call(obj) == '[object Array]'
    }
    isArray([]) //true
    isArray('haha') //false
  • 将类数组转化为数组
    1
    var trueArr = Array.prototype.slice.call(arrayLike)
  • 利用call和apply做继承
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function Person(name,age){
    this.name = name
    this.age = age
    this.sayAge = function(){
    console.log(this.age)
    }
    }
    function Male(){
    Person.call(this,name,age) //在子类构造函数中执行父类的构造函数
    }
    var haha = new Male('haha',5)
  • ######使用log代理console.log

    1
    2
    3
    4
    function log(){
    console.log.apply(console,arguments)
    }
    // 当然也有更方便的 var log = console.log()

关于bind()

也是改变函数体内this的指向

bind会创建一个新函数,称为绑定函数,当调用这个函数的时候,绑定函数会以创建它时传入bind()方法时的第一个参数作为this,传入bind()方法的第二个及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数

  • 参数的使用:
    call 是把第二个及以后的参数作为 fn 方法的实参传进去,而 fn1 方法的实参实则是在 bind 中参数的基础上再往后排。在下面的例子中,bind方法有参数’haha’,然后加上fn1传递的参数’A’, ‘B’, ‘C’,一起作为fn的参数顺序执行.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function fn(a, b, c) {
    console.log(a, b, c);
    }
    var fn1 = fn.bind(null, 'haha');

    fn('A', 'B', 'C'); // A B C
    fn1('A', 'B', 'C'); // haha A B
    fn1('B', 'C'); // haha B C
    fn.call(null, 'haha'); // haha undefined undefined

image.png

bind与apply、call最大的区别就是:bind不会立即调用,其他两个会立即调用,bind返回值是一个改变了上下文 this 后的函数.

综上:
三个使用的异同点
1.都是用来改变函数的this对象的指向的
2.第一个参数就是this要指向的对象
3.都可以利用后续参数传参
4.bind返回一个新函数,便于稍后调用,apply和call是立即调用


原型链相关问题

this是什么?

随着函数使用场合的不同,this的值会发生变化。但是有一个总的原则,那就是this指的是调用this所在函数的那个对象

2.以下代码输出什么?

1
2
3
4
5
6
7
8
var john = { 
firstName: "John"
}
function func() {
alert(this.firstName + ": hi!")
}
john.sayHi = func
john.sayHi()

输出: John: hi!
解析:函数作为对象方法调用,会使得 this 的值成为对象本身。john调用的这个函数,所以这里的this指代john对象.

3.下面代码输出什么,为什么

1
2
3
4
func() 
function func() {
alert(this)
}

输出: window对象
解析: 在函数被直接调用时this绑定到全局对象。在浏览器中,window 就是该全局对象

4.下面代码输出什么,为什么

1
2
3
4
5
6
document.addEventListener('click', function(e){
console.log(this);
setTimeout(function(){
console.log(this);
}, 200);
}, false);

输出:依次document window对象
解析:在事件处理程序中this代表事件源DOM对象,所以第一个为document,
setTimeout内的函数属于回调函数,理解为f1.call(null,f2),所以this指向window

5.下面代码输出什么,为什么

1
2
3
4
5
6
7
8
var john = { 
firstName: "John"
}

function func() {
alert( this.firstName )
}
func.call(john)

输出:John
解析: call中传入的第一个参数即this的指向

6.以下代码有什么问题,如何修改

1
2
3
4
5
6
7
8
9
10
11
12
var module= {
bind: function(){
$btn.on('click', function(){
console.log(this) //this指向$btn
this.showMsg();
})
},

showMsg: function(){
console.log('饥人谷');
}
}

修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
var module= {
bind: function(){
var _this = this
$btn.on('click', function(){
console.log(_this) //this指向module
_this.showMsg();
})
},

showMsg: function(){
console.log('饥人谷');
}
}

7.有如下代码,解释Person、 prototype、_proto__、p、constructor之间的关联。

1
2
3
4
5
6
7
8
function Person(name){
this.name = name;
}
Person.prototype.sayName = function(){
console.log('My name is :' + this.name);
}
var p = new Person("若愚")
p.sayName();

关系:

1
2
Person.prototype.constructor === Person
p.__pro__ === Person.prototype

原型图 .png

8.上例中,对象 p可以这样调用 p.toString()。toString是哪里来的? 画出原型图?并解释什么是原型链。

原型图2.png

  • 解析: p是Person构造函数的实例,p首先会从自身上找有没有toString()方法,没有的话会沿着__proto__属性找到Person.prototype对象,在这个对象上继续找有没有这个方法,如果还是没有会继续沿着Person.prototype.__proto__属性逐级往上找,直到Object.prototype为止,如果还是没有该方法,则返回null,期间找到了就调用该方法.
    p最后沿着原型链在Object.prototype中找到了toString()方法,所以就可以调用该方法,这也叫做继承方法.
  • 原型链:
    每个构造函数都有一个原型对象,原型对象都包含指向其构造函数的指针constructor,而实例都包含一个指向这个原型对象的__proto__指针。如果我们让这个原型对象等于另一个类的实例,则此时这个原型对象也将包含一个指向另一个原型对象的指针__proto__,以此为依据,层层递进,就构成了实例与原型的链条,成为原型链。
  • 原型链作用
    在访问对象的属性方法时,如果在对象本身中没有找到,则会去沿着原型链逐级向上查找,期间找到则返回该属性方法,如果这个原型链都无法找到该属性方法,则返回undefined。原型链一般实现为一个链表,这样就可以按照一定的顺序来查找,原型链是实现继承的主要方法。

注意:

当new一个类的时候,经历的步骤:

1.创建类的实例,把一个空的对象的__proto__属性设置为F.prototype(F为该类)
2.初始化实例,函数F被传入参数并调用,关键this被设定为该实例
3.返回实例对象

归纳:

1.我们通过构造函数定义了类,类自动获得属性prototype
2.每个类的实例都会有一个内部属性__proto__,指向类的prototype原型
3.一切函数都是由Function类创建的,所以Function.prototype === 被创建的函数.__proto__
4.一切函数的原型对象都是由 Object 类创建的,所以Object.prototype === 一切函数.prototype.__proto__
image.png

9.问题: 对String做扩展,实现如下方式获取字符串中频率最高的字符

1
2
3
var str = 'ahbbccdeddddfg';
var ch = str.getMostOften();
console.log(ch); //d , 因为d 出现了5次

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
String.prototype.getMostOften = function(){
var obj = {}
for(var i=0;i<this.length;i++){
var item = this.substr(i,1) //substr(start,length) 截取字符串,start截取开始位置,length截取长度
//此时item分别等于a h b b c c d e d d d d f g
if(!obj[item]){
obj[item] = 1 //如果obj对象中没有item属性,则设置item: 1;
}else{
obj[item]++ //如果有该属性item属性对应的值加1
}
}
var num = 0,max = null
for(key in obj){ //遍历obj中的属性
console.log(key) //a h b c d e f g
if(obj[key] > num){ //即某个属性对应的值大于num
num = obj[key] //最大值
max = key //最大值的属性
}
}
return max
}

var str = 'ahbbccdeddddfg';
var ch = str.getMostOften();
console.log(ch);

10.问题: instanceOf有什么作用?内部逻辑是如何实现的?

  • 作用:判断一个对象是否是某个类的实例,是的话返回true,不是的话返回false
    举例:

    1
    2
    [1,2,3] instanceof Array //true
    [1,2,3] instanceof Object //true
  • 实现:a instanceof b的判断规则是:沿着a的__proto__这条线来找,同时沿着b的prototype这条线来找,如果两条线逐级向上查找能找到同一个引用,返回true,证明A是B类型的实例,否则返回false。代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function InstanceOf(a,b){
    var proto=a.__proto__;
    do{
    if(proto === b.prototype){
    return true;
    }else{
    proto=proto.__proto__;
    }
    }
    while(proto)
    return false;
    }

继承相关问题

11.问题: 继承有什么作用?

继承是指一个对象直接使用另一对象的属性和方法。通过继承,可以使子类共享父类的属性和方法,可以覆盖(重写)和扩展父类的属性和方法,减少内存的使用,提高代码复用性。

12.问题: 下面两种写法有什么区别?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//方法1
function People(name, sex){
this.name = name;
this.sex = sex;
this.printName = function(){
console.log(this.name);
}
}
var p1 = new People('饥人谷', 2)

//方法2
function Person(name, sex){
this.name = name;
this.sex = sex;
}

Person.prototype.printName = function(){
console.log(this.name);
}
var p1 = new Person('若愚', 27);
  • 方法1是将printName()作为构造函数的方法,在实例化的时候,每次都要创建printName(),作为新对象的私有属性,这样每个实例都要重复一遍,会浪费内存
  • 方法2是将printName()作为构造函数原型上的方法,在实例化的时候,p1可以调用,其它的实例也可以来调用,是作为共享的方法,可以优化内存空间

13.问题: Object.create 有什么作用?兼容性如何?

  • 作用:
    Object.create() 方法创建一个拥有指定原型和若干指定属性的对象
    Object.create()接收两个参数,作用是创建接收到的第一个参数的副本,第二个参数是可选的、额外传入副本里的属性,以第二个参数指定的任何属性都会传入副本中并覆盖已有的同名属性,但原型对象上的同名属性不会被改变。
    如例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var person = {
    name: 'tom',
    friends: ['a', 'b', 'c']
    }

    var anotherPerson = Object.create(person, { //将anotherPerson中的__proto__替换成person
    name: { //第二个参数是额外传入的属性,会覆盖之前的同名属性
    value: 'tony'
    }
    })

    console.log(anotherPerson.name) //tony
    console.log(person.name) //tom
  • 兼容性:各大浏览器的最新版本(包括IE9)都部署了这个方法
    解决低版本iE浏览器问题:

    1
    2
    3
    4
    5
    6
    7
    if(!Object.create){
    Object.create = function(obj){
    function fn(){}
    fn.prototype = obj //prototype即方法 fn继承了obj.prototype的方法
    return new fn()
    }
    }

14.问题: hasOwnProperty有什么作用? 如何使用?

  • 作用: 判断属性和方法时自己的还是父类的,可以用hasOwnProperty,可以判断是对象自定义属性而不是原型链上的属性,返回布尔值
    hasOwnProperty是javascript中唯一一个处理属性但是不查找原型链的函数
  • 使用: 实例.hasOwnProperty('属性名'),返回true则属性存在于实例中,false则属性存在于原型中。
    1
    2
    3
    4
    5
    6
    var a = {
    name: 'a'
    };
    a.hasOwnProperty('name');//true
    a.hasOwnProperty('toString');//false
    a.hasOwnProperty('valueOf');//false

15.问题如下代码中call的作用是什么?

1
2
3
4
5
6
7
8
function Person(name, sex){
this.name = name;
this.sex = sex;
}
function Male(name, sex, age){
Person.call(this, name, sex); //这里的 call 有什么作用
this.age = age;
}
  • 作用: 绑定this指向,属性获取
    Male对象属性的获取是通过构造函数Person在其内部的执行,我们在一个类中执行另外一个类的构造函数,就可以把属性复制到自己内部,但是我们需要把环境改到自己的作用域内,就需要借助call,将this指向Male类的实例. 这样在执行Person.call(this, name, sex)会赋予实例这些属性.

16.问题: 补全代码,实现继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Person(name, sex){
this.name = name
this.sex = sex
}

Person.prototype.getName = function(){
console.log('My name is' + this.name)
};

function Male(name, sex, age){
Person.call(this,name,sex) //获取父类Person的属性,Person构造函数中的this指向实例,传入的参数this也指向实例
this.age = age //添加age属性
}

Male.prototype = Object.create(Person.prototype) //获取父类的方法
Male.prototype.constructor = Male //将constructor有person类重新绑定到Male类

Male.prototype.getAge = function(){
console.log('My age is' + this.age)
};

var ruoyu = new Male('haha', '男', 27);
ruoyu.getName();

总结:

1.获取父类属性
方法: 在子类的构造函数中,执行父类的构造函数,借助call绑定this指向实例

1
2
3
function 子类(){
父类.call(this,arguments) //this指向子类的实例,执行一遍后获取到父类的属性
}

2.获取父类的方法
方法一般都设置在类的prototype中,所以从这点出发
实现: 借助Object.create()方法,将子类.prototype.__proto__ = 父类.prototype

1
2
子类.prototype = Object.create(父类.prototype)   //设置完后,此时 子类.prototype.constructor为父类,需纠正
子类.prototype.constructor = 子类