对象基础

对象前置知识

/* 对象基础知识 */

var teacher = {
name: '张三',
age: 32,
sex: 'male',
height: 176,
weight: 130,
teach: function(){
console.log('I am teaching JS');
},
smoke: function(){
console.log('I am smoking');
},
eat: function(){
console.log('I am having a dinner');
}
}
/* 查找 */
console.log(teacher.name);
teacher.teach();
teacher.smoke();
teacher.eat();

/* 增加 */
teacher.address = '北京'
teacher.drink = function(){
console.log('I am drinking beer');
}
console.log(teacher.address);
teacher.drink();

/* 修改 */
teacher.teach = function(){
console.log('I am teaching Vue');
}
teacher.teach();

/* 删除 */
delete teacher.address;
delete teacher.teach;
console.log(teacher);

构造函数基础

从上文我们可以知道,创建对象的一种方式,通过 {} (即对象字面量)来创建。下面我们来讲讲采用构造函数方式创建。

第一种,通过系统自带的构造函数

var obj = new Object();
obj.name = '张三';
obj.sex = '男士';
console.log(obj);

这种方式通过系统自带的构造函数实例化出来的,其实是和对象字面量一样,没啥区别。

对象和构造函数不能混淆,对象是通过实例化构造函数而创建的。这里不知道小伙伴们理不理解,下文会探讨这个问题的。

第二种,自定义构造函数

对于自定义构造函数,我们一般采用大驼峰命名(单词首字母全大写),里面一个关键词 this,考一考,此时 this 指向谁?指向 Teacher吗?

/* 自定义构造函数 采用大驼峰命名*/
function Teacher(){
this.name = '张三';
this.sex = '男士';
this.smoke = function(){
console.log('I am smoking')
}
}

答案是 this 根本不存在,因为函数在 GO里面,里面内容根本不会看,如下:

GO = {
Teacher: function(){...}
}

因此 this都没有生成,并且 Teacher是构造函数。而如果想要 this存在,就需要实例化,因为上文提到的,this它是指向的对象本身。因此,需要如下一行代码,进行实例化操作。

var teacher = new Teacher();

好的,那么我们现在对上述代码进行一丢丢修改,看下面代码会打印什么?

function Teacher(){
this.name = '张三';
this.sex = '男士';
this.smoke = function(){
console.log('I am smoking')
}
}

var teacher1 = new Teacher();
var teacher2 = new Teacher();

teacher1.name = '李四';
console.log(teacher1.name);
console.log(teacher2.name);

答案是 李四 张三。因为通过构造函数 new 出来的两个对象根本不是一个东西,是两个不同的对象,因此更改某一个完全不影响另外一个对象。也就是说构造函数实例化的多个对象相互之间是不影响的。

下面给出一份封装构造函数的基础代码:

function Teacher(opt){
this.name = opt.name;
this.sex = opt.sex;
this.weight = opt.weight;
this.course = opt.course;
this.smoke = function(){
this.weight--;
console.log(this.weight);
}
this.eat = function(){
this.weight++;
console.log(this.weight);
}
}

var t1 = new Teacher({
name: '张三',
sex: '男士',
weight: 130,
course: 'JS'
});
var t2 = new Teacher({
name: '李四',
sex: '女士',
weight: 90,
course: 'Vue'
});
console.log(t1);
console.log(t2);

包装类

主要就是这三种:

new Number new String new Boolean

举个简单例子,小伙伴们应该就能明白了。

var a = 'abc';
console.log(a);

var aa = new String('abc');
aa.name = 'aa';
console.log(aa);

var bb = aa + 'bcd';
console.log(bb);

答案是 abc [String: 'abc'] abcbcd,包装类参与运算的时候会转换成原始值参与运算。补充:原始值不会有属性和方法

再来一道例题吧,下面输出会有结果吗?

var a = 123;
a.len = 3;
console.log(a.len);

var b = 'abc';
console.log(b.length);

答案是 undefined 3,这个时候就会有疑惑了,上文不是说原始值不会拥有属性和方法嘛,那 b.length是怎么肥事呢?这就涉及到包装类的问题了。

对于第一个输出,这里解释一下,首先原始值不会有属性和方法,而js在执行到 a.len = 3的时候,会进行一次包装,即 new Number(3).len = 3; 然而它仅仅只是赋值操作,也没有办法进行保存,赋值完后,执行 delete操作删除,最后当我们访问 a.len 的时候打印 undefined了。总体来说,相当于如下操作:

var obj = {
name: 'Chocolate'
}
console.log(obj.name); // Chocolate
delete obj.name;
console.log(obj.name); // undefined

以上就是包装类的过程。

对于第二个输出,也来解释一下。有了上一题分析,我想你们也会想到包装类了,这里是字符串,我们不妨打印一下 new String() 会有怎样的结果:

发现没有,包装类里面有一个 length属性,因此当我们 js 执行时,遇到 b = 'abc',也会进行一层包装,然后将长度存储到 length属性上,因此我们就能访问得到,而上一题我们没办法存储,最后也就被删除掉了。

补充知识点

数组的截断方法:

var arr = [1,2,3,4,5];
arr.length = 3;
console.log(arr); // 1 2 3

继续来做一道题,看看会输出什么:

var name = 'Chocolate';
name += 10;
var type = typeof(name);
if(type.length === 6){
type.text = 'string';
}
console.log(type.text);

答案是 undefined,原理和上文代码一致。这里就不详细解释了,不太懂的小伙伴可以往上看看下面这个例子。

var a = 123;
a.len = 3;
console.log(a.len);

那么,怎么输出 string呢?其实,我们可以自己包装一个就可以了。

var name = 'Chocolate';
name += 10;
var type = new String(typeof(name)); // 重点在这
if(type.length === 6){
type.text = 'string';
}
console.log(type.text); // string

接着,继续,来一道经典的笔试题,看看下面三个会输出什么?

function test(a,b,c){
var d = 1;
this.a = a;
this.b = b;
this.c = c;
function f(){
d++;
console.log(d);
}
this.g = f;
}
var test1 = new test();
test1.g(); //
test1.g(); //
var test2 = new test();
test2.g(); //

答案:2 3 2。解释一下,其实在 test函数最后会有一个默认返回,即 return this。因此也就形成了一个闭包, test函数的 AO也被带出去了,这个和累加器原理一样。然后对于实例化的两个对象,它们互不影响,所以d都是从1作为初始值。

下面来一道综合题,回顾上篇的知识,下面三个函数哪些会打印 1 2 3 4 5呢?

function foo1(x){
console.log(arguments);
return x;
}
foo1(1,2,3,4,5);

function foo2(x){
console.log(arguments);
return x;
}(1,2,3,4,5);

(function foo3(x){
console.log(arguments);
return x;
})(1,2,3,4,5);

答案是 foo1 foo3。这里只解释一下foo2为啥不能打印,因为对于函数声明后面跟着括号 (),如果没有传参的话,就会报错,传参了,它会返回以逗号分割的最后一个元素。

继续,又是一个阿里的笔试原题,看看会输出什么?

function b(x,y,a){
a = 10;
console.log(arguments[2]);
}
b(1,2,3);

答案是 10,因为上篇就有介绍过,对于实参传形参,如果实参和形参有映射关系,那么我们就可以修改实参,否则没办法修改实参。

原型基础

原型 prototype 其实是 function对象的一个属性,但是打印出来结果它也是对象。


那我们直接看下面这个例子吧

function Foo(name,age){
this.name = name;
this.age = age;
}
Foo.prototype.sex = '男士'

var foo = new Foo('Chocolate',21);
console.log(foo.name); // Chocolate
console.log(foo.sex); // 男士

拓展:prototype是定义构造函数构造出的每个对象的公共祖先,所有被该构造函数构造出的对象都可以继承原型上的属性和方法。

原型的作用,如上述代码一样,将一些配置项写在构造函数里,对于一些写死的值或者方法,就可以直接挂载到原型上去,可以减少代码冗余。

知识点补充:

实例的 __proto__其实就是一个容器,就是为了在对象里面给 prototype设置一个键名。

来一道简单题吧,会输出什么?

function Car() { };
Car.prototype.name = 'Math';
var car = new Car();
Car.prototype.name = 'Benz';
console.log(car.name);

答案是 Benz,相当于进行了一次覆盖操作。

现在,我进行一点点修改,看看又会输出什么?

Car.prototype.name = 'Benz';

function Car() { };
var car = new Car();

Car.prototype = {
name: 'Math'
}
console.log(car.name);

答案是 Benz,实例化一个car对象,首先car.name先去找构造函数找对应 name 属性,没有找到,然后就去原型对象上去找,找到对应name值为 Benz,赋值。继续往下走,发现有对原型对象重定义的操作,但是此时实例对象早就通过原本构造函数 new出来了。(简单来说,就是再定义了一个 prototype,但是没有实例化)

可能不太好理解上述表达,我们对上述代码修改一丢丢,看看又会打印什么?

Car.prototype.name = 'Benz';

function Car() {};
Car.prototype = {
name: 'Math'
}
var car = new Car();

console.log(car.name);

答案是 Math,因为你此时重新定义了构造函数的 prototype,并且进行了实例化。

可能你会想到这个例子,这里只是更改了属性,并不是重写

function Car() { };
Car.prototype.name = 'Math';
var car = new Car();
Car.prototype.name = 'Benz';
console.log(car.name);

原型链基础

下面我们就要开始讲解原型链相关了,直接看下面这个例子吧:

Professor.prototype.tSkill = 'Java';
function Professor(){}
var progessor = new Professor();

Teacher.prototype = progessor;
function Teacher(){
this.mSkill = 'js';
}
var teacher = new Teacher();

Student.prototype = teacher;
function Student(){
this.pSkill = 'html';
}

var student = new Student();

console.log(student);

原型链就是像如下例子,沿着 __protp__这条线往上找相应的原型的属性值的链条,这就是原型链。

补充,原型本身也有原型,但是原型链不可能一直链接,因此,会有一个顶端。原型链的顶端Object.prototype。因为Object也是有原型的。并且 Object.prototype保存了一个 toString()方法。

继续,我们对上述代码进行一点修改,然后我们修改student实例对象里面的属性值,看是否teacher实例对象也会发生变化?

Professor.prototype.tSkill = 'Java';
function Professor(){}
var progessor = new Professor();

Teacher.prototype = progessor;
function Teacher(){
this.mSkill = 'js';
this.success = {
alibaba: '28',
tencent: '30'
}
}
var teacher = new Teacher();

Student.prototype = teacher;
function Student(){
this.pSkill = 'html';
}

var student = new Student();
student.success.baidu = '100';
student.success.tencent = '50';
console.log(teacher,student);

结果:

上述问题明白之后,我们再来看看下面这道题,看看又会有什么变化?

Professor.prototype.tSkill = 'Java';
function Professor(){}
var progessor = new Professor();

Teacher.prototype = progessor;
function Teacher(){
this.mSkill = 'js';
this.students = 500;
}
var teacher = new Teacher();

Student.prototype = teacher;
function Student(){
this.pSkill = 'html';
}

var student = new Student();
console.log(student.students);
student.students++;
console.log(student, teacher);


从结果我们发现,只有 student实例对象底下的 students变成了 501,而 teacher 实例对象下面的 students没有变化。因为对于原始值而言, student对象底下没有 students这个属性,于是就会创建一个,然后自加。上一题是拿到了引用地址,于是可以修改,这道题意思和如下代码类似:

let obj = {
name: 'Chocolate'
}
obj.age = 21; // obj没有age属性,于是创建一个。
console.log(obj.age);

注意,一般不推荐按照如上两种方式修改原型对象上的属性值,后文会详细介绍继承的方式,这里只是抛砖引玉。

继续,看下一题,一道经典的笔试题:

function Car(){
this.brand = 'Benz';
}
Car.prototype = {
brand: 'Mazda',
intro: function(){
console.log('我是' + this.brand + '车');
}
}
var car = new Car();
car.intro();

答案是 我是Benz车,首先new出来一个实例对象,然后访问实例的 intro()方法,发现没找到,于是会沿着原型链往上找,发现存在,然后打印。关键是 this.brand,因为 this会指向这个实例,实例访问的话,会首先访问由对应构造函数实例出来的对象,发现存在,直接打印。

Object.creat()基础

之前了解到了 Object,现在我们探讨一下底下的一个方法 create(),它仍然可以创建对象,但是和普通创建对象又不太一样。下面来简单分析一下:

Obeject.create(xxx); // xxx处可以指定自定义原型或者填写null
function Obj(){
}
Obj.prototype.num = 1;
var obj1 = Object.create(Obj.prototype);
console.log(obj1);

打印出来,此时实例原型的constructor指向这个 Obj()。好的,那我们看看用 new出来的实例对象,有没有什么区别?

function Obj(){
}
Obj.prototype.num = 1;
var obj1 = Object.create(Obj.prototype);
var obj2 = new Obj();
console.log(obj1);
console.log(obj2);

看看下面结果,发现其实没啥区别,因为都是根据Obj的原型创建出来的。

继续,看看下面这种情况:

var obj1 = Object.create(null);
console.log(obj1);

通过Object.create(null)创建出来的对象,里面啥也没有,这里也就说明了一个点,虽然原型链的顶端是Object.prototype,但是这个特殊的空对象并没有原型,它不会继承于 Object.prototype,因此,不是所有的对象都继承于 Object.prototype

注意,我们没办法自造 __proto__,创建了之后只是相当于属性值一样,实例对象是没有办法调用原型对象上的方法的。

var obj = Object.create(null);
obj.num = 1;
var obj1 = {
count: 2
}
obj.__proto__ = obj1;
console.log(obj);
console.log(obj1);

看下面这张图,有咩有发现什么不同,对于我们自造的 __proto__颜色更深有没有?

现在探究一下自己造的 __proto__能不能访问到原型对象上的东西。

var obj = Object.create(null);
obj.num = 1;
var obj1 = {
count: 2
}
obj.__proto__ = obj1;
console.log(obj.count);

结果是 undefined,显然没有办法访问,也就证明我们没办法自造 __proto__

好的,上文都是创建了一些对象,下文我们探讨一下特殊例子,比如nullundefined

上文有一个结论,我们发现除开空对象外都能继承Object.prototype,然后访问其中一个方法 toString(),那么nullundefined可以吗?我们测试一下:

console.log(null.toString()); // TypeError: Cannot read property 'toString' of null
console.log(undefined.toString()); // TypeError: Cannot read property 'toString' of undefined

那么,为啥下面这个代码会输出 1呢?

var num = 1;
console.log(num.toString()); // 1

在解释之前,我们再来回顾一下知识点:

原始值是没有属性的,为啥能调用 toString()方法,就是本文目录第三块讲解的包装类的概念了。

它的工作过程如下:

首先 new Number(1),然后再调用toString()方法,因此之前new了一下成为了对象。为了更加准确,我们打印一下看看。

var num = 1;
console.log(num.toString());

var num2 = new Number(num);
console.log(num2);

发现new了之后,在 __proto__里面确实找到了 toString()方法。

回到开头,nullundefined为啥不可以呢?就是因为上文提过nullundefined没办法进行包装。始终为原始值,并且没有原型,也没办法继承。

下面我们探讨一下隐式转换性和继承相关问题,代码如下:

var num = 1;
var obj = {};
var obj2 = Object.create(null);
document.write(num);
document.write(obj);
document.write(obj2);

然后我们发现最后一个打印有了报错:不能转换为原始值,这是因为啊,obj2创建的空对象没有继承Object.prototype,因此也就没有对应 toString()方法。当然不能转换了。(document.write()方法转换为字符串)

call / apply

面试必备的知识点 call / apply,现在好好探究一下。

先来热热身,看如下样例:会输出什么?

function Car(brand,color){
this.brand = brand;
this.color = color;
}
var newCar = {};
Car.call(newCar,'Benz','red');
console.log(newCar);

答案是 { brand: 'Benz', color: 'red' },发现没有,这里将 this指向改变了。

apply的使用如下,打印结果和上题一样,也是将 this指向改变了。

function Car(brand,color){
this.brand = brand;
this.color = color;
}
var newCar = {};
Car.apply(newCar,['Benz','red']);
console.log(newCar);

链式调用基础

给出如下代码,你如何进行修改,让最后一行代码都能执行呢?

var sched = {
wakeup: function(){
console.log('Running');
},
work: function(){
console.log('Wordking');
},
end: function(){
console.log('Ending');
}
}
sched.wakeup().work().end();

答案如下:

函数每次返回 this,这种做法类似于 Jquery里面的链式调用。

var sched = {
wakeup: function(){
console.log('Running');
return this;
},
work: function(){
console.log('Wordking');
return this;
},
end: function(){
console.log('Ending');
return this;
}
}
sched.wakeup().work().end();

继续,看下面代码,补充一个知识点:

var myLang = {
No1: 'HTML',
No2: 'CSS',
No3: 'JS',
myStudying: function(num){
console.log(this['No'+num]);
}
}
myLang.myStudying(1);

答案是 HTML,显而易见,主要是说明如下知识点,在早起JS引擎就是这样访问对象属性的,通过 obj[name]中括号形式访问,现在继承了 obj.name的形式,但是最终解释时还是会转换成 obj[name]的形式。

对象枚举

开门见山,我们直接来一道题,看看下面两种方式打印有区别吗?还是都可以打印?

var car = {
brand: 'Benz',
color: 'red',
displacement: '3.0',
lang: '5',
width: '2.5'
}
for(var key in car){
console.log(car.key);
console.log(car[key]);
}

答案是 car.key没有办法访问属性值,返回的都是 undefined,而car[key]可以。因为当我们访问 cay.key时,JS引擎会这样做:

car.key -> car['key'] -> undefined

下面,我们来探究一下 hasOwnProperty这个方法。

在讲解方法之前,先来看看如下代码,会输出什么?

function Car(){
this.brand = 'Benz';
this.color = 'red';
this.displacement = '3.0';
}

Car.prototype = {
lang: 5,
width: 2.5
}

Object.prototype.name = 'Object';

var car = new Car();
for(var key in car){
console.log(key + ':' + car[key]);
}

答案如下:

brand:Benz
color:red
displacement:3.0
lang:5
width:2.5
name:Object

诶,我们发现了一个问题,当我们访问car实例对象的时候,原型链上所有的属性我们都访问出来了。那么我想要打印自己构造函数里面的属性值而不要原型链上的该怎么做呢?于是就印出来 hasOwnProperty

现在修改一下代码:

function Car() {
this.brand = 'Benz';
this.color = 'red';
this.displacement = '3.0';
}

Car.prototype = {
lang: 5,
width: 2.5
}

Object.prototype.name = 'Object';

var car = new Car();
for (var key in car) {
if (car.hasOwnProperty(key)) {
console.log(key + ':' + car[key]);
}
}

此时打印结果如下,发现只打印自己构造函数里面的属性值,没有打印原型链上的

brand:Benz
color:red
displacement:3.0

接下来,我们再来探究另外一个重要的东西,instanceof

开门见山,还是以例题来热身:

function Car(){}
var car = new Car();

function Person(){}
var p = new Person();

console.log(car instanceof Car);
console.log(car instanceof Object);
console.log([] instanceof Array);
console.log([] instanceof Object);
console.log({} instanceof Object);

答案是全为true,解释一下,A instanceof B,就是用来判断 A对象原型里面有没有 B的原型。也就是原型链上重合的都为 true

this指向

接下来,又是一个重点,我们探究一下 this指向问题。

开门见山,我们还是来一道简单题热热身,看看下面会有输出吗?会输出什么?

function test(b){
this.d = 3;
var a = 1;
function c(){}
}
test(123);
console.log(d);

答案是 3,对于函数内部的this,如果没有进行实例化操作,this会指向 window。外部也可以访问。

总结归纳一下:

  • 全局 this 指向 window
  • 预编译函数 this 指向 window
  • apply / call 改变 this 指向
  • 构造函数的 this 指向实例化的对象

接下来,介绍一个平常容易忽视但确实用的比较少的知识:callee / caller的区别。

直接看下面例题,看会打印什么?

function test(a,b,c){
console.log(arguments.callee.length);
}
test(1,2,3);

答案: 3,解释一下,arguments.callee会返回实参列表所对应的函数(即 test),然后执行 test.length(即形参的个数 3)。

callee 还有什么用呢,例如下述代码,在匿名自执行函里面,我们得不到对应函数名,而使用 callee可以用作递归,获取 arguments对应的函数来递归执行。

var sum = (function(n){
if(n<=1){
return 1;
}
return n + arguments.callee(n-1);
})(10);
console.log(sum); // 55

下面讲解一下 caller,这个更少见,并且严格模式下还会报错,小伙伴们了解一下即可。

直接看下面例子,看会打印什么:

test1();
function test1(){
test2();
}
function test2(){
console.log(test2.caller);
}

答案是 [Function: test1],解释一下, test2.caller结果就是谁执行了 test2,就会打印对应的那个函数。

真题演练

不知不觉,又总结了许多知识。下面我们好好练一练真题,巩固一下。

第一题

首先,依旧是热热身,看看会输出什么?

function foo() {
bar.apply(null, arguments);
}
function bar() {
console.log(arguments);
}
foo(1, 2, 3, 4, 5);

答案如下:

这里就相当于在 foo 函数里面执行了 bar,然后给它传了参数。this指向在bar函数里面没有使用,传null值也不影响。

第二题

JStypeof 可能返回的值有哪些?

答案如下:

number string undefined object function boolean 

第三题

看看下面会输出什么?

function b(x,y,a){
arguments[2] = 10;
console.log(a);
}
b(1,2,3);

答案是 10,实参和形参映射关系,如果有映射,那么我们可以修改实参。

那么我们稍微修改一下上述代码,又会是怎样的结果呢?

function b(x,y,a){
a = 10;
console.log(arguments[2]);
}
b(1,2,3);

答案还是10,与上题思路一样,不作解释了。

继续,下面这道题之前有出过,再来温习一下:

var f = (
function f(){
return '1';
},
function g(){
return 2;
}
)
console.log(typeof f);

答案是 function,有没有和我一样以为是 number的小伙伴,还是不能太自信,粗心了。简单解释一下,对于括号表达式里面,以逗号分隔的话,会返回最后一个。

那么我把上述代码稍作修改一下,又会输出什么呢?

var f = (
function f(){
return '1';
},
function g(){
return 2;
}
)()
console.log(typeof f);

答案显然是 number,因为执行了,不作过多解释了。

第四题

下面打印true的是哪些?(序号以 1开头)

console.log(undefined == null);
console.log(undefined === null);
console.log(isNaN('100'));
console.log(parseInt('1a') == 1);

答案是 1 4,这里只解释一下 parseInt,它只会去从左到右的数字,一遇到非数就截止了。

可能这几个隐式转换不算很难,下面再来几个,继续,加油!

这个又会输出什么呢?

console.log({} == {});

答案是 false,因为引用值对应的是地址,地址不同,肯定不等。怎么让两个空对象相等呢,可以按照如下方式做:

var obj = {};
var obj1 = obj;
console.log(obj == obj1); // true

看一道输出题吧:

var a = '1';
function test(){
var a = '2';
this.a = '3';
console.log(a);
}
test();
new test();
console.log(a);

答案是 2 2 3

提升一下,最后一题:

var a = 5;
function test(){
a = 0;
console.log(a);
console.log(this.a);
var a;
console.log(a);
}
test();
new test();

答案如下:

test(); // 0 5 0
new test(); // 0 undefined 0

简单解释一下,先给出 AOGO吧。

Go = {
a: undefined -> 5,
test: function(){...}
}
AO = {
a: undefined -> 0
}

这里只解释为啥第二个中间打印 undefined,因为它 new 出来的实例,this 当然指向它,但是 this 上面没有 a 这个属性,所以打印 undefined