对象
- 对象是一种数据类型
- 对象是以键值对存储数据的一种容器(属性和方法)
var per = {
name: "张三",
age: 18,
sayHi: function() {}
};
- 对象是对现实世界单个实物的一种抽象
如对一辆车的描述 特征:品牌 颜色 型号 行为…
var car = {
brand: "奔驰",
color: red,
driving: function() {}
};
- 对象是现实世界与程序世界沟通的一座桥梁
- 万物皆对象
面向对象
- 面向对象与面向过程不是一门技术
- 面向对象是一种解决问题的思维方式(面向过程 面向对象)
- 面向过程注重的是过程 面向对象注重的是结果
- 面向对象的本质是对面向过程的封装
- 面向对象编程 通过原生 js
将面向过程封装成对象 实现代码复用 避免与全局变量污染的问题 解决代码冗余
// 将函数封装成对象 避免与全局变量污染的问题 解决代码冗余
var obj = {
getTag: function(str) {
return document.getElementByTagName(str);
},
setStyle: function(list) {
for (var i = 0; i < list.length; i++) {
list[i].style.backgroundColor = "red";
}
}
};
var pList = obj.getTag("p");
//通过对象调用方法
obj.setStyle(pList);
- 面向对象编程 通过 jquery 方法
$("p").css("backgroundColor", "red");
通过 new 关键字的函数 都是构造函数
所有的对象都是构造函数创建的
内置对象也是构造函数创建的
-
[]
底层是调用了 new Array() -
{}
底层是调用了 new Object() -
function
底层是调用了 new Function() -
Date
底层是调用了 new Date() -
自定义构造函数
function Person(name, age) {
this.name = name;
this.age = age;
// 不同对象调用构造函数中的方法 不是同一个函数
this.sayHi = function() {
console.log("我的名字" + this.name + "我的年龄" + this.age);
};
}
//实例化对象(通过构造函数创建对象的过程)
//由于预解析 会将变量提升 所以会先开辟p1空间 在执行new关键字的四个步骤
var p1 = new Person("张三", 18);
var p2 = new Person("李四", 20);
/*new关键字工作原理
1.创建一个空对象
2.将this关键字指向这个空对象
3.执行构造函数代码 完成对象的赋值
4.自动返回这个对象
*/
// 不同对象调用构造函数中的方法 不是同一个函数
console.log(p1.sayHi == p2.sayHi); //false
//构造函数中的属性存的是数据 而方法存的是地址 不同对象的地址不同 所以不是同一个函数
//弊端: 每调用一次构造函数 就会创建一个新的函数 浪费内存资源
- 解决方案
将构造函数中的方法提取出来 解决内存资源浪费 - 弊端
提取出来的方法 在全局作用域中 会造成全局变量污染
function fn() {
console.log("我的名字" + this.name + "我的年龄" + this.age);
}
function Person(name, age) {
this.name = name;
this.age = age;
// 将构造函数中的方法提取出来 引用类型赋值拷贝的是地址
this.sayHi = fn;
}
//实例化对象(通过构造函数创建对象的过程)
//由于预解析 会将变量提升 所以会先开辟p1空间 在执行new关键字的四个步骤
var p1 = new Person("张三", 18);
var p2 = new Person("李四", 20);
/*new关键字工作原理
1.创建一个空对象
2.将this关键字指向这个空对象
3.执行构造函数代码 完成对象的赋值
4.自动返回这个对象
*/
// 不同对象调用构造函数中的方法 是同一个地址 是同一个函数
console.log(p1.sayHi == p2.sayHi); //true
- 解决方案
使用对象解决全局变量污染
var obj = {
sayHi: function() {
console.log("我的名字" + this.name + "我的年龄" + this.age);
}
};
//此时不能只用用函数式 要用函数表达式 因为函数式会将整个函数提升 函数里面obj是undefined 所以会报错
/* function Person(name,age){
this.name=name;
this.age=age;
this.sayHi= obj.fn;
}; */
// 函数表达式 变量只会变量名提升 其他在原地 所以不会报错
var Person = function(name, age) {
this.name = name;
this.age = age;
// 引用类型赋值拷贝的是地址 但是如果需要创建狗狗的构造函数 又要创建一个与之对应的对象来存储方法
//也就是说每次创建一个构造函数 就要声明一个与之对应的对象来存储方法
this.sayHi = obj.sayHi;
};
//实例化对象(通过构造函数创建对象的过程)
//由于预解析 会将变量提升 所以会先开辟p1空间 在执行new关键字的四个步骤
var p1 = new Person("张三", 18);
var p2 = new Person("李四", 20);
/*new关键字工作原理
1.创建一个空对象
2.将this关键字指向这个空对象
3.执行构造函数代码 完成对象的赋值
4.自动返回这个对象
*/
// 不同对象调用构造函数中的方法 是同一个地址 是同一个函数
console.log(p1.sayHi == p2.sayHi); //true
- 综上所述 以上方法都有弊端
- 最终解决方案
使用原型对象就可以解决前面所有遇到的弊端
原型对象
- 所有构造函数在声明之后 系统会自动帮我们创建一个与之对应的对象 这个对象称之为原型对象
- 同时解决内存资源浪费 与 全局变量污染的问题
- 构造函数中有一个
prototype
属性 存储的是原型对象的地址 所以构造函数指向与之对应的原型对象 - 获取原型对象
构造函数名.prototype
- 通过点语法 动态给原型对象添加属性和方法
- 原型对象有一个
constructor
属性 指向构造函数
var Person = function(name, age) {
this.name = name;
this.age = age;
};
//构造函数的原型中动态添加成员
Person.prototype.sayHi = function() {
console.log("我的名字" + this.name);
};
//构造函数访问原型对象中的成员
Person.prototype.sayHi();
//由这个构造函数实例化的每一个对象
var p1 = new Person("张三", 18);
p1.sayHi();
var p2 = new Person("李四", 20);
p2.sayHi();
//都指向原型中的成员 都是一个方法
console.log(p1.sayHi == p2.sayHi); //true
console.log(p1.sayHi == Person.prototype.sayHi); //true
实例化对象
- 实例化对象有个
__proto__
属性 不是W3C的标准属性 有些浏览器不支持 - 指向与之对应的构造函数的原型对象
__proto__
属于构造函数的 prototype
console.log(p1.__proto__ === Person.prototype);//true
-
构造函数与原型对象与实例化对象之间的三角关系
-
构造函数prototype属性指向原型对象
-
原型对象construct属性指向构造函数
-
实例化对象__proto__属性指向与之对应构造函数的原型对象
-
__proto__
属于构造函数的 prototype
console.log(p1.__proto__ === Person.prototype);//true
- 构造函数与原型对象与实例化对象之间的三角关系
- 构造函数prototype属性指向原型对象
- 原型对象construct属性指向构造函数
- 实例化对象__proto__属性指向与之对应构造函数的原型对象
原型中constructor属性的注意点
如果修改了原型对象 constructor属性会丢失
解决方案 手动添加constructor执行构造函数名
function Stu(name, age) {
this.name = name;
this.age = age;
}
//点语法给原型对象动态添加成员
Stu.prototype.study = function () {
console.log("学习ing");
};
var s1 = new Stu('张三', 20);
s1.study();
console.log(s1);
//修改原型的指向
Stu.prototype = {
study() {
console.log('修改了');
},
//constructor属性丢失 手动添加即可
constructor: Stu
}
var s1 = new Stu('张三', 20);
console.log(s1.constructor);
所有实例对象共有的成员都可以放在原型对象中
- 对象访问成员的规则
先看自己有没有 有则访问 没有则看原型对象中有没有 有则访问
function Stu(name, age) {
this.name = name;
this.age = age;
}
//原型中有study方法
Stu.prototype.study = function () {
console.log("原型对象中的方法");
};
var s1 = new Stu('张三', 20);
//实例化对象中的study方法
s1.study = function () {
console.log('实例化对象中的方法');
}
s1.study();
- 实例化对象访问原型对象中成员的规则
取决于实例化对象是在什么实例化的
修改前实例化 访问修改前的原型对象成员
修改后实例化 访问修改后的原型对象成员
function Stu(name, age) {
this.name = name;
this.age = age;
}
//原型中有study方法
Stu.prototype.study = function () {
console.log("原型对象中的方法");
};
//实例化对象
// 在修改之前实例化 访问修改前的原型对象中的成员
// var s1 = new Stu('张三', 20);
// s1.study();
//修改原型对象的指向
Stu.prototype = {
study: function () {
console.log('修改后的原型对象');
}
}
// 在修改之后实例化 访问修改后的原型对象中的成员
var s1 = new Stu('张三', 20);
s1.study();
- 总结
-
.原型对象
不管是哪个构造函数被创建 系统都会自动帮我们生成一个与之对应的原型对象 -
访问原型对象的方法
构造函数名.prototype
-
谁可以访问原型对象
构造函数自身
构造函数实例化出来的对象 -
原型对象使用的注意点
① 构造函数实例化对象们共有的数据 才可以放在原型对象中
② 构造函数实例化的对象们 访问成员的规则 就近原则(先看自己的 自己有就用自己的 自己没有就看原型有没有)
③ 原型可以修改
实例化对象访问原型看是实例化对象是在修改原型之前还是在修改原型之后
④原型对象存储所有时候对象共有的数据 避免了内存资源浪费与全局变量污染 -
__proto
属于实例化对象 指向构造函数与之对应的原型对象
可以让同一个构造函数所有的实例化对象拥有共同的成员 -
constructor
属于原型对象 指向与之对应的构造函数
通过constructor
属性可以知道某个实例化对象是由哪个构造函数创建的
- 面向对象的三大特征
- 封装
将功能封装到对象中 外部只管调用 无需关系内部实现 - 继承
一个对象拥有另一个对象的所有成员 - 多态
同一个对象在不同情况的多种状态
- ★继承★
- 方法1 混入式
弊端 每继承一次 就要执行forin循环
//方法1 混入式
// 弊端 每继承一次 就要执行forin循环
var father = {
car: {
price: 30000,
color: 'red'
}
}
var son = {
}
//son继承father
for (var key in father) {
//son中点语法动态添加成员
son[key] = father[key]
}
console.log(son);
- 方法2 替换原型
弊端 直接赋值 会丢失之前的原型对象
//方法2 替换原型
var father = {
car: {
price: 30000,
color: 'red'
}
}
function Son(gf) {
this.gf = gf;
}
//将父对象作为子对象构造函数的原型对象
//那么实例化对象都拥有父对象的所有成员
//弊端 直接赋值 会丢失之前的原型对象
Son.prototype = father;
var s1 = new Son(['小乔', '大桥']);
console.log(s1);
console.log(s1.car);
var s2 = new Son(['小新', '大新']);
console.log(s2);
console.log(s2.car);
- 方法3 混合式(混入式+替换原型)
遍历父对象所有的成员 添加到子对象的原型中
//方法3 混合式(混入式+替换原型)
var father = {
car: {
price: 30000,
color: 'red'
}
}
function Son(gf) {
this.gf = gf;
}
//遍历父对象所有的成员 添加到子对象的原型中
for (var key in father) {
Son.prototype[key] = father[key];
}
var s1 = new Son(['小乔', '大桥']);
console.log(s1);
console.log(s1.car);
var s2 = new Son(['小新', '大新']);
console.log(s2);
console.log(s2.car);
- 将混合式继承函数封装
//混合式继承的封装
var father = {
car: {
price: 30000,
color: 'red'
}
}
function Son(gf) {
this.gf = gf;
}
//method子对象的构造函数
//father父对象
function extend(method, father) {
for (var key in father) {
method.prototype[key] = father[key];
}
}
//调用
//第一个参数传入构造函数 第二个参数传入父对象
extend(Son, father);
- ★多态★
function Animal(name) {
this.name = name;
}
var dog = new Animal('狗砸');
var cat = new Animal('喵咪');
dog.eat = function (food) {
console.log('我是' + this.name + '我喜欢吃' + food);
}
cat.eat = function (food) {
console.log('我是' + this.name + '我喜欢吃' + food);
}
//人
function Person(name) {
this.name = name;
}
var p = new Person('人');
p.work = function (animal, food) {
animal.eat(food)
};
//调用
//同一个对象在不同情况的多种状态
p.work(dog, '鸡腿')
p.work(cat, '喵粮')
- 原型链
每一个构造函数都有自己的原型对象 原型对象也是对象 (对象都有一个__proto__
属性) 也有自己的原型对象 以此类推 形成的链式结构称之为原型链 - 对象访问原型链中成员的规则
就近原则
(对象访问成员 先看自己有没有 有则访问 没有则从原型寻找 如果原型有则访问 原型没有 就从原型的原型寻找 以此类推 直到原型链的终点null 如果都没有 属性返回undefined 方法返回程序报错)
function Person(name, age) {
this.name = name;
this.age = age;
}
var p = new Person('人', 20);
console.log(p.gender);//对象取值 不存在返回undefined
// p.say();//undefined没有方法 所以报错
console.log(p);
console.log(p.toString());//[object Object]
//p中没有toString方法 原型中也没有 但是原型的原型中有toString方法 所以不会报错
console.log(p.constructor);//Person构造函数
console.log(p.__proto__ === Person.prototype);//true
console.log(p.__proto__.__proto__.constructor);//Object
console.log(p.__proto__.__proto__ === Object.prototype);//true
console.log(p.__proto__.__proto__.__proto__);//null
内置对象的原型链
- Date日期对象原型链
var date = new Date();
//日期对象不能用log 会自动转成toString字符串
//查看对象内存空间 用dir
console.dir(date)
console.log(date.constructor);//Date构造函数
console.log(date.__proto__ === Date.prototype);//true
console.log(date.__proto__.__proto__.constructor);//Object
console.log(date.__proto__.__proto__ === Object.prototype);//true
console.log(date.__proto__.__proto__.__proto__);//null
- 数组对象的原型链
var arr = [10, 20, 30];
//底层用了new Array()构造函数
// var arr = new Array([10,20,30]);
console.log(arr)
console.log(arr.constructor);//Array构造函数
console.log(arr.__proto__ === Array.prototype);//true
console.log(arr.__proto__.__proto__.constructor);//Object
console.log(arr.__proto__.__proto__ === Object.prototype);//true
console.log(arr.__proto__.__proto__.__proto__);//null
- String对象原型链
var str = '123';
//底层用了基本包装类型new String()构造函数
// var str = new String('123');
console.log(str)
console.log(str.constructor);//String构造函数
console.log(str.__proto__ === String.prototype);//true
console.log(str.__proto__.__proto__.constructor);//Object
console.log(str.__proto__.__proto__ === Object.prototype);//true
console.log(str.__proto__.__proto__.__proto__);//null
- DOM对象的原型链
- 函数也是对象类型 是一个特殊的对象 所有的函数都是由Function构造函数创建的
- 也可以通过点语法动态添加成员
function fn() {
console.log('fn函数');
}
fn.say = function () {
console.log('函数动态添加方法');
}
fn.show = '函数动态添加属性'
// 下面不报错说明函数也是对象类型 是特殊的对象
fn.say();
console.log(fn.show);
console.log(fn);//打印函数中存储的代码
console.dir(fn);//打印函数对象中存储的属性
console.log(fn.__proto__.constructor);//Function构造函数
//得出函数都是由Function构造函数创建的
console.log(Object.__proto__.constructor);//Function构造函数
console.log(Array.__proto__.constructor);//Function构造函数
console.log(fn.__proto__ === Function.prototype);//true
console.log(fn.__proto__.__proto__.constructor);//Object
console.log(fn.__proto__.__proto__ === Object.prototype);// true
console.log(fn.__proto__.__proto__.__proto__);//null
- 拓展
- Function构造函数创建对象 参数都是字符串 有很多个参数
- 前面所有的参数都是函数的形参
- 最后一个参数是函数体代码
//函数的两种声明方式
//1.函数声明
function fn(a, b) {
return a + b;
}
//2.表达式声明
var fn1 = function (a, b) {
return a + b;
}
//函数的底层都是new Function
//Function构造函数
//前面所有参数都是函数的形参 最后一个参数是函数体代码
//参数都是字符串
var fn2 = new Function('a', 'b', 'return a+b');
var res = fn2(10, 20);
console.log(res);
//所以js所有构造函数对象 都是Function构造函数生成的
完整的原型链
- js中所有的对象都是构造函数创建的
1.原型对象由Object构造函数创建
2.函数对象由Function构造函数创建
3.实例化对象由对应的构造函数创建 - 只要是构造函数 就有
prototype
属性指向与之对应的原型对象 - 只要是原型对象 就有
constructor
属性指向对应的构造函数 - 只要是对象 就有
__proto__
属性指向对应的构造函数的原型对象 - 函数也是对象 js中所有的函数对象都是由Function构造函数创建的
//自定义构造函数创建
function Person(name, age) {
this.name = name;
this.age = age;
}
var p = new Person('张三', 20);
console.log(p);
//内置构造函数创建
//{}底层new Object()
var obj = {}
instanceof
运算符
- 语法
对象 instanceof 构造函数
检查右边构造函数的原型 在不在左边对象的原型链中
//判断Object在不在Array的原型链中
//Array函数对象 都是Function创建的
//Array->Function->Object->null
console.log(Array instanceof Object);//true
console.log(Array instanceof Function);//true
console.log(Array instanceof Array);//false
//Function函数对象 都是由Function创建的
//Function->Function->Object->null
console.log(Function instanceof Function);//true
console.log(Function instanceof Object);//true
//Object函数对象 由Function创建
//Object->Function->Object->null
console.log(Object instanceof Function);//true
console.log(Object instanceof Object);//true
函数的三种调用方式
- 全局函数
//全局函数 this指向window 因为fn全局函数是window的一个属性
function fn() {
console.log(this);//window
}
// window.fn();
fn();
- 对象方法
//对象的方法
var p = {
name: '张三',
age: 15,
sayHi() {
console.log(this);//p对象
console.log(this === p);//true
}
}
p.sayHi();
//将fn函数赋值给对象的sayHi
p.sayHi = fn;
p.sayHi();//此时执行fn中代码 this指向p
- 构造函数
//构造函数 new会做四件事 会将this指向new创建的对象
function Stu(name, age) {
this.name = name;
this.age = age;
}
var s = new Stu('李四', 18);
console.log(s);
//普通函数 相当于window.Stu()调用 this是window 给window动态加属性
var s1 = Stu('王五', 20);
console.log(s1);//没有返回值 结果undefined
console.log(name);//王五
console.log(age);//20
this谁调用 this指向谁
全局函数中 this指向window
对象方法中 this指向对象
构造函数中 this指向new创建的对象
定时器中 this默认指向window
- 函数的三种调用方式的共同点
this的指向是确定的无法修改的
函数的上下文调用
- 可以动态修改this的指向
- call()
语法函数名.call(修改this的指向,arg1,arg2...)
function fn(a, b) {
console.log(this);
console.log(a + b);
}
fn(1, 2);//this指向window
fn.call({ name: '张三' }, 10, 20);//修改后this指向这个对象
- apply()
语法函数名.apply(修改this的指向,[arg1,arg2...])
第二个参数是数组或伪数组 会自动遍历数组或伪数组的每一个元素给函数的形参赋值
function fn(a, b) {
console.log(this);
console.log(a + b);
}
fn(1, 2);//this指向window
fn.apply([1, 2], [30, 40]);//修改后this指向这个数组
-
bind()
语法函数名.apply(修改this的指向,arg1,arg2...)
bind()不会执行这个函数 而是会返回一个修改了this之后的新函数 然后再对这个新函数进行传参bind ()修改this的指向一般用于回调函数 如定时器
function fn(a, b) {
console.log(this);
console.log(a + b);
}
fn(1, 2);//this指向window
//bind与call不同的是 它不会执行这个函数
//而是会返回一个修改了this之后的函数
//然后在堆返回的函数传参
//bind一般用于回调函数中 如定时器
var fn2 = fn.bind([2, 22, 222]);
fn2(1, 11)
//定时器的this默认指向window 可以用bind修改this的指向
//匿名函数
setTimeout(function () {
console.log(this);
//回调函数中修改this 此时this执行对象
}.bind({}), 2000);
//表达式函数
var test = function () {
console.log(this);
}
setTimeout(test.bind([10, 20]), 1000);
- 上下文调用模式三种语法的异同点
- 相同点: 作用都是动态修改this的指向
- 不同点传参方式不一样
- 上下文调用模式三种方法 其实是属于Function构造函数的原型的 意味着js中所有的函数都能使用上下文调用修改this的指向
- 修改的this只能是引用类型
如果将this改为值类型 会自动转成基本包装类型
如果将this改为undefined和null 程序不会报错 也不会修改 this依旧指向window
如果没有传参 this依旧指向window
- 伪数组转数组
方法1 声明空数组 遍历伪数组 往空数组中追加
//伪数组转数组
var weiArr = {
0: 10,
1: 20,
2: 30,
length: 3
}
//方法1
var arr =[];
for(var i=0;i<weiArr.length;i++){
arr.push(weiArr[i])
}
console.log(arr);
方法2 声明空数组 通过apply自动遍历伪数组
//方法2
var weiArr = {
0: 10,
1: 20,
2: 30,
length: 3
}
var arr = [];
arr.push.apply(arr, weiArr)
console.log(arr);
方法3 通过数组的slice()方法
传入0 返回数组自身
通过数组的原型调用slice方法
通过call修改this的指向
//方法3
var weiArr = {
0: 10,
1: 20,
2: 30,
length: 3
}
// var res = arr.slice(0);//会返回一个新数组 传入0 返回自身
// console.log(res);
var res = Array.prototype.slice.call(weiArr, 0);
console.log(res);
- 伪数组排序
//伪数组排序
var weiArr = {
0: 20,
1: 1,
2: 5,
3: 2,
4: 10,
length: 5
}
Array.prototype.sort.call(weiArr, function (a, b) {
return a - b;
})
console.log(weiArr);
- 求数组最大值
方法1 打擂台
//数组最大值
//方法1
var arr = [200, 50, 3, 60, 9, 66];
var max = arr[0];
for (var i = 0; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i]
}
}
console.log(max);
方法2 Math.max() 用apply将参数变成数组
//方法2
//Math.max([],[]...) 用apply将参数变成数组
console.log(Math.max.apply(Math,arr));
思考题: 数组调用toString结果与对象调用toString结果为什么不一样??
因为Array的原型中有toString Object的原型中也有toString
根据变量访问原型链成员的构造 如果数组调用toString会访问Array原型中的toString
//Array.prototype.toString() 底层调用的是 数组.join()
//Object.prototype.toString() 返回固定的字符串 [Object 数据类型]
console.log([].toString());//空字符串
console.log({}.toString());//[object Object]
console.log([1,2,3].toString());//1,2,3
console.log({name:'张三'}.toString());//[Object Object]
万能数据类型检测法
typeof
关键字检测null和array都会得到object 无法检测- 原理: Object.prototype.toString()会返回一个固定格式的数据类型字符串 只需要通过call修改this的指向 就可以检测所有的数据类型
console.log(Object.prototype.toString.call(null));//[object Null]
console.log(Object.prototype.toString.call([]));//[object Array]
借用构造函数继承
借用Person中的两行代码赋值给Stu函数 通过call修改this为Stu函数的this
只能继承this点出来的属性 如果是实例化点出来的属性无法继承
function Person(name, age) {
this.name = name;
this.age = age;
}
var p = new Person('张三', 18);
console.log(p);
function Stu(name, age) {
// this.name = name;
// this.age = age;
// Person(name,age);
//借用构造函数继承 通过call修改this为Stu函数中的this
Person.call(this, name, age);
}
var s = new Stu('学生', 18)
console.log(s);
静态成员与实例成员
- 静态成员: 属于构造函数对象自身的成员
- 实例成员: 属于构造函数实例化对象的成员
function Person(name, age) {
this.name = name;
this.age = age;
}
//由构造函数点出来的就是静态成员
Person.aaa = 'aaa'
var p = new Person('张三', 18);
//由实例化对象点出来的是实例成员
console.log(p.name);
console.log(p.age);
Object.prototype
所有的对象的原型链最终都会指向Object.prototype
意味着js中所有对象都可以访问Object.prototype中的成员
- Object.prototype中的成员
hasOwnProperty
检测对象是否拥有某个成员
对象.hasOwnProperty('属性')
返回值布尔类型
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.type = '哺乳动物';
var p = new Person('张三', 18);
console.log(p);
//hasOwnProperty 检测对象是否拥有某个成员
//对象.hasOwnProperty('属性') 返回值布尔类型
console.log(p.hasOwnProperty('name'));//true
console.log(p.hasOwnProperty('type'));//false type是原型的 不是p自己的
isPrototypeOf
检测一个对象是不是另一个对象的原型
对象a isPrototypeOf(对象b)
检测b的原型是不是a 返回值布尔值
console.log(Person.prototype.isPrototypeOf(p));//等价于 p.__proto__ === Person.prototype 结果是true
propertyIsEnumerable
检测某个对象是否可以枚举某个属性(枚举 是自己的成员 可以被forin遍历)
对象.propertyIsEnumerable('属性')
// propertyIsEnumerable 检测某个对象是否可以枚举某个属性
//对象.propertyIsEnumerable('属性')
//枚举 是自己的成员 可以被forin遍历
//对象可以被遍历
for(var key in p){
console.log(p[key]);
}
//数组虽然可以被遍历 但是有一个length属性不能被遍历
var arr = [10,20,30];
for(var key in arr){
console.log(arr[key]);
}
console.log(arr.hasOwnProperty('length'));//true 数组中有length属性
console.log(arr.propertyIsEnumerable('length'));//false 数组中length属性不能被遍历 就是不能被枚举 所以false
函数对象的常用属性
- 函数也是对象 也有自己的默认成员
构造函数中的成员是静态成员 caller
获取调用这个函数的引用length
获取形参的个数name
获取函数的名字arguments
获取所有的实参- arguments是伪元素 也是对象 对象有自己的属性
length
获取实参的个数callee
获取函数自身(用于匿名函数递归调用)
//函数也是对象
//函数中也有自己默认成员 构造函数中的成员是静态成员
console.dir(fn1)
//caller 获取调用这个函数的引用(我被谁调用了)
//如果函数b中调用函数a 那么函数a的caller就是函数b
//如果是全局调用(window调用) 那么函数的caller就是null
function fn1() {
console.log('哈哈');
console.log(fn1.caller);//fn2
}
function fn2() {
fn1();//fn1的caller就是fn2
console.log('嘿嘿');
}
fn2();
fn1();//如果是全局调用 window调用 那么fn1.caller就是null
//length 获取形参的个数
function fn3(a, b) {
console.log(fn3.length);//2
}
fn3(10, 20, 30, 40)
//name 获取函数的名字
function fn4() {
console.log(fn4.name);//fn4
}
fn4()
//arguments 获取所有的实参
function fn5() {
//arguments伪数组 本质是对象 也有自己的属性callee获取函数自身 length获取实参的个数
console.log(fn5.arguments);
console.log(fn5.arguments.length);//获取实参的个数
console.log(fn5.arguments.callee);//获取函数自身 fn5 用于匿名函数递归调用
console.log(fn5.length);//获取形参的个数
}
fn5(10, 20, 30, 40)
call与caller与callee的区别(重点)
call 是属于Function.prototype 作用是修改函数中this的指向
caller 是属于函数对象的成员 作用是获取调用这个函数的引用(我被谁调用了)
callee 是属于arguments对象的 作用是获取函数本身 应用于匿名函数递归调用
递归
- 函数自己调用自己
- 递归函数的特点
1.能用递归函数实现的功能一定能用循环调用函数来实现
2.一定要有结束条件 否则也会导致死循环 - 递归语法
//循环
for(var i =1;i<=3;i++){
fn1()
}
function fn1(){
console.log('循环调用了');
}
//递归
var i = 1;
function fn() {
console.log('递归调用了');
i++;
if (i <= 3) {
fn()
}
}
fn()
- 递归嵌套
function fn1(){
fn2()
}
function fn2(){
fn1()
}
- 求累加和
//求1-n的累加和
//循环
function getSum(n) {
var sum = 0;
for (var i = 1; i <= n; i++) {
sum += i;
}
return sum;
}
console.log(getSum(3));
//递归
function getSum(n) {
if (n == 1) {
return 1;
} else {
return getSum(n - 1) + n
}
}
console.log(getSum(3));
- 求阶乘
//求阶乘
// 1! 1
//2! 2*1 2*getJi(1)
//5! =5*getJi(4)
//循环
function getJi(n) {
var ji = 1;
for (var i = 1; i <= n; i++) {
ji *= i;
}
return ji;
}
console.log(getJi(5));
//递归
function getJi(n) {
if (n == 1) {
return 1;
} else {
return getJi(n - 1) * n
}
}
console.log(getJi(5));
//阶乘的另一种写法
//arguments.callee函数递归调用
var num = (function (n) { return n == 1 ? 1 : arguments.callee(n - 1) * n })(5)
console.log(num);
- 斐波那契数列
//斐波那契数列
//1 1 2 3 5 8 13 21 34 ...
//循环
function getFei(n) {
var arr = [1, 1];
for (var i = 2; i < n; i++) {
arr[i] = arr[i - 2] + arr[i - 1]
}
return arr[arr.length - 1]
}
console.log(getFei(9));
//递归
function getFei(n) {
if (n == 1 || n==2) {
return 1;
} else {
return getFei(n - 2) + getFei(n-1)
}
}
console.log(getFei(9));
- 遍历DOM树
//获取father父元素的所有后代元素
//遍历父元素 遍历每一个子元素 找到子元素的子元素 继续遍历 以此类推
//声明一个空数组 存储所有的后代元素
var list = [];
function houDai(ele) {
for (var i = 0; i < ele.children.length; i++) {
list.push(ele.children[i]);
//递归求元素的子元素
houDai(ele.children[i])
}
}
houDai(father);
console.log(list);
//获取DOM树
houDai(document);
console.log(list);
js的变量的作用域
- 全局作用域(全局变量)
在函数外部声明的变量 - 全局变量的生命周期 (变量从声明到销毁 )
页面从打开到关闭 - 局部作用域(局部变量)
在函数内部声明的变量 - 局部变量的生命周期
开始调用函数到函数执行完毕 会自动被系统回收
如何访问函数内部的变量
- 可以通过return 返回一个值 函数外部用变量接收
- 但是如果有多个变量接收同一个返回值 访问的不是同一个对象
- 每调用一次函数 就会生成一个新的对象 造成了内存资源的浪费
function fn() {
var obj = {
name: '张三',
age: 18
}
//要想获取函数中的变量 通过return 值
return obj;
}
//函数外部用变量接收这个返回值
var a = fn();
var b = fn();
//但是多个变量接收的返回值不是同一个对象
console.log(a == b);//false
- 闭包
是一个可以在函数外部访问函数内部变量的函数 - 闭包作用
可以在函数外部访问函数内部变量
延长局部变量的生命周期 - 闭包语法
在外部函数outer的内部声明一个闭包函数closure
在闭包函数内部返回想要访问的局部变量
在外部函数outer中返回闭包函数 - 闭包的本质
是一个沟通函数内部(局部作用域)与函数外部(全局作用域)的桥梁 - 闭包的唯一弊端
延长了局部变量的生命周期 造成不必要的资源浪费
解决方案 闭包设置为null即可释放闭包内存
function fn() {
var obj = {
name: '张三',
age: 18
}
function closure() {
//这里可以访问fn函数中的变量
return obj;
}
//返回闭包函数
return closure;
}
//bibao变量获取的是闭包函数
var bibao = fn();
//闭包函数调用 得到fn中的局部变量
var a = bibao();
var b = bibao();
//多个变量接收的返回值是同一个对象
console.log(a == b);//true
- 注意
- 如果希望闭包访问的是同一个变量 外部函数只能调用一次
- 如果希望闭包访问的是不同的变量 外部函数需要调用多次
function outer() {
var num = Math.floor(Math.random() * 100)
function closure() {
//这里可以访问outer函数中的变量
return num;
}
//返回闭包函数
return closure;
}
//bibao变量获取的是闭包函数
//调用一次outer函数 访问同一个变量
var bibao = outer();
//闭包函数调用
var a = bibao();
var b = bibao();
console.log(a);
console.log(b);
//调用多次outer函数 访问的是不同的变量
var bibao1 = outer();
var c = bibao1();
console.log(c);
var bibao2 = outer();
var d = bibao2();
console.log(d);
- 闭包语法注意点
- 调用一次outer函数 相当于一台投票机 每调用一次closure函数 就往这台投票机投票
- 调用多次outer函数 相当于多台投票机 每调用一次closure函数 各投一票
function outer() {
var num = 10;
function closure() {
//这里可以访问outer函数中的变量
num++;
console.log(num);
return num;
}
//返回闭包函数
return closure;
}
//bibao变量获取的是闭包函数
//调用一次outer函数 访问同一个变量
var bibao = outer();
//闭包函数调用 调用closure函数 每调用一次 num++
//相当于一台投票机 投2票
bibao();//11
bibao();//12
//调用多次outer函数 访问的是不同的变量
//相当于 两天投票机 各投一票
var bibao1 = outer();
bibao1();//11
//上面两个式子等同于outer()()
var bibao2 = outer();
bibao2();//11
- 点击li标签显示索引
- 方法1 原生js
//点击li元素显示索引
//方法1 原生js
var liList = document.getElementsByTagName('li');
for (var i = 0; i < liList.length; i++) {
liList[i].setAttribute('index', i);
liList[i].onclick = function () {
alert(this.getAttribute('index'))
}
}
- 方法2 闭包
//方法2 闭包
var liList = document.getElementsByTagName('li');
for (var i = 0; i < liList.length; i++) {
function outer() {
var num = i;
function closure() {
alert(num)
}
return closure;
}
// var bibao = outer();
// liList[i].onclick = bibao;
//简写
liList[i].onclick = outer()
}
- 3个定时器 1s后分别打印1 2 3
//开3个定时器 1s后分别打印1 2 3
//定时器中的代码不是立即执行的 而是等页面所有代码加载完毕 1s之后才执行 此时循环早已走完
//方法1
for (let i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(i);
}, 1000)
}
//方法2
for (var i = 1; i <= 3; i++) {
function outer() {
var num = i;
setTimeout(function () {
console.log(num);
}, 1000)
function closure() {
return num
}
return closure;
}
outer()
}
//方法2简写
for (var i = 1; i <= 3; i++) {
function outer() {
var num = i;
return function closure() {
console.log(num);
}
}
setTimeout(outer(), 1000)
}
- 斐波那契数列
- 方法1 循环
//斐波那契数列
//原生 循环(数组中性能稍差 需要存储多个元素 浪费内存)
function fib(n) {
var arr = [1, 1];
for (var i = 2; i < n; i++) {
arr[i] = arr[i - 2] + arr[i - 1]
}
return arr[arr.length - 1]
}
var res = fib(10);
console.log(res);
- 方法2 递归(性能差 需要重复计算)
//递归(性能差 需要重复计算)
function getFib(n) {
if (n == 1 || n == 2) {
return 1;
} else {
return getFib(n - 2) + getFib(n - 1)
}
}
var res = getFib(10);
console.log(res);
- 方法3 使用第三个元素占位空间 存储最终的结果
//第三个元素占位空间 存储最终的结果
//每调用一次fib() 内部数组都要重新声明 所以此时要使用闭包延长数组的生命周期
function fib(n) {
var arr = [1, 1, 0];
for (var i = 2; i < n; i++) {
arr[2] = arr[0] + arr[1];
arr[0] = arr[1];
arr[1] = arr[2];
}
return arr[2]
}
console.log(fib(10));
- 方法4 闭包操作(性能最好)
//闭包
function fib() {
var arr = [1, 1, 0];
var closure = function(n){
for (var i = 2; i < n; i++) {
arr[2] = arr[0] + arr[1];
arr[0] = arr[1];
arr[1] = arr[2];
}
return arr[2]
}
return closure;
}
var bibao = fib();
console.log(bibao(10));
//但是 再次调用需要释放闭包的内存
bibao = null;
var bibao = fib();
console.log(bibao(10));
- 重点(面试题)
var name = 'My Window';
var obj = {
name: 'My Object',
getFunction() {
//返回是到全局中 所以this指向window
return function () {
console.log(this.name);//My Window
}
}
}
obj.getFunction()();//My Object
var name = 'My Window';
var obj = {
name: 'My Object',
getFunction() {
//方法的this是obj 所以that是obj对象
//闭包会延长that的生命周期
var that = this;
return function () {
console.log(that.name);//My Object
}
}
}
obj.getFunction()();