对象基础
对象前置知识
/* 对象基础知识 */
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__
。
好的,上文都是创建了一些对象,下文我们探讨一下特殊例子,比如null
和 undefined
。
上文有一个结论,我们发现除开空对象外都能继承Object.prototype
,然后访问其中一个方法 toString()
,那么null
和 undefined
可以吗?我们测试一下:
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()
方法。
回到开头,null
和 undefined
为啥不可以呢?就是因为上文提过null
和 undefined
没办法进行包装。始终为原始值,并且没有原型,也没办法继承。
下面我们探讨一下隐式转换性和继承相关问题,代码如下:
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
值也不影响。
第二题
JS
中 typeof
可能返回的值有哪些?
答案如下:
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(); // 0 5 0
new test(); // 0 undefined 0
答案如下:
test(); // 0 5 0
new test(); // 0 undefined 0
简单解释一下,先给出 AO
和 GO
吧。
Go = {
a: undefined -> 5,
test: function(){
...}
}
AO = {
a: undefined -> 0
}
这里只解释为啥第二个中间打印undefined
,因为它new
出来的实例,this
当然指向它,但是this
上面没有 a
这个属性,所以打印 undefined
。