原型,原型链,call/apply
文章目录
原型(prototype)
1.定义:
原型是function对象的一个属性,它定义了构造函数制造出的对象的公共祖先。通过该构造函数产生的对象,可以继承该原型的属性和方法。原型也是对象。
// 函数也相当于一个对象 是对象就有属性和方法
Person.prototype // 给函数增加属性方法 prototype:原型(也是一个对象)
// Person.prototype = {} 在函数刚刚出生的时候就已经被定义好了 它是一个空对象 可以理解为这个空对象就是构造函数构造对象的爹 是祖先
Person.prototype.name = "hehe"; // 给空对象加属性
Person.prototype.say = function () { // 定义一个方法
console.log('haha')
}
function Person() { // 构造函数的特点:大驼峰式(其他函数时小驼峰)
this.name = "zhang" // 如果函数自己定义了name属性,就会取自身的值,没有的情况下才会取祖先的值(跟AO/GO性质一样)
}
var person = new Person(); // 构造函数可以产生多个相似且独立的对象
// person是构造的对象 会继承祖先的属性 person中没有name属性 但是person.name会输出"hehe" person.say() 输出haha (加小括号是函数执行)
var person1 = new Person(); // person1也会继承祖先的属性 person1.name也会输出hehe person1.say() 输出haha
Person.prototype.LastName = "hehe";
Person.prototype.say = function () {
console.log('haha')
}
function Person(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
var person = new Person('zhangjie', 3, 'male'); // 对象有了自己的属性 也有继承来的属性(LastName) 访问person.LastName输出hehe
2.利用原型特点和概念,可以提取共有属性。
// 引用一:提取共有部分放到原型里面
// 创建多个对象都会执行中间三行没传参的代码,会造成代码冗余(耦合) 所以需要用到prototype给原型赋值 这个操作只会执行一遍 然后对象通过继承获取数据
Car.prototype.carName = "BMW";
Car.prototype.height = 1400;
Car.prototype.lang = 4900;
/*
因为prototype就是一个空对象,所以上面的写法可以写成
Person.prototype = {
carName : "BMW";
height : 1400;
lang : 4900;
}
*/
function Car(color, owner) {
this.owner = owner;
this.color = color;
}
// 这里访问到的carName、height、lang都是通过原型继承来的
var car = new Car('red', 'Cheng');
var car1 = new Car('green', 'Ye');
Person.prototype.lastName = "Cheng";
function Person(name) {
this.name = name;
}
var person = new Person('Jaosn')
person.lastName = "XieNa" // person是没有lastName属性的,这样写只会在person对象中增加lastName属性,通过对象无法修改原型的东西
Person.prototype.lastName = "Zhang"; // 只能通过原型自己来修改
// 无法通过构造的对象去增删改原型的 但是可以查
delete person.name // 返回true 因为可以删除构造函数自身的属性
delete person.lastName // 通过构造函数删除原型的lastName 返回true
person.lastName // 但是访问lastName还在
// 这是因为构造函数只会在自己内部找lastName 没有这个属性会认为是没有的属性 删除没有的属性会返回true
delete person.abc // 比如这个也会返回true 因为person也没有abc属性
3.对象如何查看原型—>隐式属性__proto__
__proto__
:原型链,指向的是索引,也就是原型(构造对象的爹)
__proto__
的object对象中的__proto__
中保存着prototype
// __proto__的作用
// 在查找一个属性时,如果构造的对象自身没有这个属性,就会查找__proto指向的索引找想要的属性
Person.prototype.name = 'abc'
function Person() {
/* var this = {
__proto__ : Person.prototype
} */
}
var person = new Person();
person.name // 这里访问person.name 但是person没有name属性 就会通过__proto__指向的索引找到Person.prototype中的name属性
// 更改__proto__会怎样
Person.prototype.name = 'abc'
function Person() {
}
var obj = {
name : 'sunny'
}
var person = new Person();
person.__proto__ = obj // 更改构造对象的__proto__属性的指向
person.name // 再访问name就会输出sunny 因为继承的原型被修改了
Person.prototype.name = 'Jason' // 给原型加属性
// var _private 在变量前加_说明这个变量有其他用途 提醒同事别乱改
function Person() {
}
/* __proto__里面放的就是原型
在new的时候发生隐式三段式
1. var this = { 这个this并不是空的 里面有__proto__属性
__proto__ : Person.prototype
如果构造的对象在自身找不到要找的属性 就会通过__proto__指向的索引去找需要的属性 起到一个连接的作用
}
*/
var person = new Person(); // 控制台查看person会发现构造的对象person里面有一个隐式属性__proto__ 后面的object对象 对象里面有constructor指向Person(构造函数) 还有一个__proto__
Person.prototype.name = 'sunny';
function Person() {
}
Person.prototype.name = 'cherry'; // Person.prototype.name = 'cherry';写在构造函数之前和之后,访问person.name输出的都是cherry
var person = new Person();
// Person.prototype.name = 'cherry'; 访问person.name输出也是cherry
// 原型是个对象 属于引用值,本体存在栈中,__proto__拿到的就是指引它的房间号。新的原型在栈中重新开辟了一个新的房间,但__proto__拿着的还是老原型在栈中的房间号
Person.prototype.name = 'sunny';
function Person() {
// var this = {__proto__ : Person.prototype} 这里指向的是之前的房间 之前的房间放的是sunny
}
var person = new Person();
Person.prototype = { // 这里是新创建了一个房间放cherry
name : 'cherry'
}
person.name // 输出sunny 因为__proto__还是指向原来的房间
// 上面的过程可以简化成以下三步
1.Person.prototype = {name : 'a'}; // 一开始定义了name属性
2.__proto__ = Person.prototype; // 在name还是a的房间赋值给了__proto__ 所以__proto__存储的就是name=a
3.Person.prototype = {name : 'b'}; // prototype重新建了一个房间放b 但是__proto__指向的是a房间
// 换个位置看看
Person.prototype.name = 'sunny';
function Person() {
}
Person.prototype = {
name : 'cherry'
}
var person = new Person();
person.name // 输出cherry 因为一开始的房间放sunny,但是又在构造对象之前新建了一个房间放cherry 就会把新建的房间赋值给__proto__
4.对象如何查看对象的构造函数—> constructor
constructor指向的是构造函数的函数体
Car.prototype // prototype(原型)在函数一出生就有
function Car() {
}
var car = new Car();
// constructor:构造器 系统自带的属性 指的的构造函数的函数体 function Car() {} 目的是构造的对象能找到自己的构造函数(自己的爹)
car.constructor // 会返回构造这个对象的构造函数
Car.prototype // prototype(原型)在函数一出生就有
function Car() {
}
var car = new Car();
// constructor:构造器 系统自带的属性 指的的构造函数的函数体 function Car() {} 目的是构造的对象能找到自己的构造函数(自己的爹)
car.constructor // 会返回构造这个对象的构造函数
// 尝试修改constructor,让他认贼作父
function Person() {
}
Car.prototype = {
constructor : Person; // 修改成功(constructor可手动修改)
}
function Car() {
}
var car = new Car()
car.constructor // 访问car.constructor就是Person这个构造函数
原型链__proto__
如何构成原型链?
把原型连成链,访问顺序是依照链的顺序,像作用域一样的东西叫原型链。原型链的连接点就是prototype。
原型链就是以对象为基准,以 __proto__
为连接的这条链,一直到 Object.prototype
为止的这条链 叫做原型链。
原型链就是 以对象原型为节点连起来的一条链条。
__proto__指向原型路径,可读不可写(可以访问但是不能人为添加__proto__属性)
// Grand也不是原型链的头 Object.prototype才是所有对象的最终原型 所以Object.prototype中没有__proto__ 因为到头了
//Grand.prototype.__proto__ = Object.prototype
Grand.prototype.lastName = "Deng";
function Grand(){
}
var grand = new Grand();
Father.prototype = grand; // father构造对象的原型是grand(构造对象)
function Father() {
this.name = 'xuming'
}
var father = new Father();
Son.prototype = father // son构造对象的原型是father(构造对象)
function Son() {
this.hobbit = 'smoke'
}
var son = new Son();
console.log(son.hobbit) // 打印smoke
console.log(son.name) // 打印xuming 自身没有name属性 就往原型father上找
console.log(son.lastName) // 打印Deng father也没有lastName属性 就往father的原型grand上找
son.toString // 会返回Object.prototype对象中的toString返回 因为那是原型链的终端
原型链上属性的增删改查
基本与原型一致
Grand.prototype.lastName = "Deng";
function Grand(){
}
var grand = new Grand();
Father.prototype = grand;
function Father() {
this.name = 'xuming';
this.fortune = {
card1 : 'vise'
};
this.num = 100
}
var father = new Father();
Son.prototype = father
function Son() {
this.hobbit = 'smoke'
}
var son = new Son();
// 增加原型链
father.prototype.abc = "111" // 只能自己增加自己的原型链属性
son.prototype.abc = "111" // 无法增加不是自己的原型链属性 这样写只会给son的原型增加abc属性
// 删除原型链
delete father.prototype.name // 只能自己删除自己的原型链属性
delete son.prototype.name // 无法删除不是自己的原型链属性
// 修改原型链
father.prototype.name = "wang" // 只能自己修改自己的原型链属性
son.prototype.name = "wang" // 无法修改不是自己的原型链属性
<一种特殊的修改方式>
son.fortune = 200 // 这样只会给son增加fortune这个属性
son.fortune.car2 = 'master' // 调用fortune然后增加car2属性 这是引用值自身的修改 所以会修改fortune中的值
son.num ++ // father中的num还是100 这里是把father中的num取出来给son 然后++ 所以son.num中的值是101
// 查看原型链
<一直往上找,直到找到需要的属性,如果终端都没有,就会返回null>
// xxx.sayName
// sayName里面的this指向: 谁调用这个方法,this就指向谁
Person.prototype = {
name : "a",
sayName : function () {
console.log(this.name);
}
}
function Person() {
}
var person = new Person();
person.sayName // 打印a 因为自己没有,所以指向原型中的sayName
Person.prototype = {
name : "a",
sayName : function () {
console.log(this.name);
}
}
function Person() {
this.name = "b"
}
var person = new Person();
person.sayName // 打印b 自己又sayName 就打印自己的sayName
Person.prototype = {
height : 100
}
function Person() {
this.eat = function () {
this.height ++;
return 123; // 不设return值会默认返回undefined
}
}
var person = new Person();
person.eat() // 调用eat方法之后访问person会发现增加了一个height属性 值是101 原型上的height还是100 无法修改原型的属性值 只能改自己的
// 凡是对象都有原型 对象字面量写法更简单 所以基本上都用这种写法var obj = {}
1. var obj = {} // 系统内部会new Object()
2. var obj1 = new Object() // 这两种写法效果一样
// obj1.proto__ ----> object.prototype obj1的构造函数时object 所以obj1的__proto__存储的原型路径就是object.prototype 这也是原型链的终端 所以会继承toString方法
Person.prototype = {} -->object.prototype
function Person() {
}
绝大多数对象的最终都会继承自Object.prototype
Object.create(null) // null 不是对象 不能做原型 这样写返回结果是Object {} 但是这个对象里面啥都没有 构造的这个对象没有原型
var obj = Object.create(null); // obj没原型
obj.toString // 访问报错 因为没原型所以没有toString方法
obj.__proto__ = {name : "sunny"} // obj是对象 可以认为添加__proto__属性 但是这个属性是显式的(自己定义的 不是系统定义的)
obj.name // 返回undefined 因为人为定义的__proto__系统不会读
var obj = Object.create(123); // 报错 原始值不行 得是Object.prototype(原型)
// undefined和null不能使用toString方法,因为他们没有原型
123.toString //能调用toString方法是因为他们会通过包装类一层层向上访问,包装类包装起来可以是个对象,对象的终端是Object.prototype
Object.create(原型)
// Object.create()也能创建对象 括号里面必须写原型
// var obj = Object.create(原型),用obj接收创建的对象
// 这种创建对象的方法更加灵活
var obj = {name : "sunny", age : 123};
var obj1 = Object.create(obj) // obj1的原型就是obj
Person.prototype.name = "sunny";
function Person() {
}
var person = Object.create(Person.prototype) // 写构造函数的原型
toString
// 不是对象的调用toString方法
true.toString() // 返回"true"
var num = 123; num.toString(); // 返回“123”
num.toString(); -->包装类:new Number(num).toString();
/*
new Number(num) Number就是num的构造函数 所以num的原型是Number.prototype
Number.prototype上面就有toString方法 Number.prototype.toString = function () {
}
所以num.toString调用的是自己重写的名字相同但是功能不同的toString方法
*/
Number.prototype.__proto__ = Object.prototype // Number的原型是Object.prototype
123.toString(); // 会报错 因为.可以表示计算符或者调用 但是计算符.优先级最高 所以这里会被认定为是浮点型float 所以系统会认为.后面是数字 浮点型后面加字母是会报错的 所以数字需要先转换成变量 num = 123
// 对象调用的toString是Object.prototype上的toString
var obj = {};
obj.toString(); // 返回“[[object Object]]”
Person.prototype = {
}
function Person () {
}
var person = new Person();
person.toString(); // 访问的是终端的toString方法 返回的是"[[object Object]]"
// 重写(写一个名字一样但是功能不同的方法)
Person.prototype = {
toString : function () {
return "hehe";
}
}
function Person () {
}
var person = new Person();
person.toString(); // 访问的是Person自己定义的toString方法 返回hehe
// 或者直接修改系统自带的toString方法
Object.prototype.toString = function () {
return 'haha';
}
Person.prototype = {
}
function Person () {
}
var person = new Person();
person.toString(); // 返回haha
call
Object.prototype.toString // 原生的toString方法
Number.prototype.toString // 重写的toString方法
Array.prototype.toString // 重写的toString方法
Boolean.prototype.toString // 重写的toString方法
String.prototype.toString // 重写的toString方法
// 如果已经重写了toString怎么访问系统原生的toString方法?
Object.prototype.toString.call(123) // call的意思就是让123去调用Object.prototype.toString(终端)这个方法 返回"[object Number]" 真正顶端的toString其实没什么用 只会返回"[object 类型]" 所以子孙重写方法
Object.prototype.toString.call(true) // 返回"[[object Boolean]]"
Number.prototype.toString = function () {
return '成哥身体好';
}
num = 123;
num.toString(); //返回成哥身体好 调用自己重写的toString方法
// document.write输出的并不是这个东西,而是输出这个东西调用方法后的结果
var obj = {}
document.write(obj); // 打印[object Object] 说明了document.write会调用隐式的toString方法
document.write(obj) ==> document.write(obj.toString());
var obj = Object.create(null); // 让对象变成自己创建出来的create(null)
document.write(obj); // 报错 因为null没有原型 更没有toString方法
var obj = Object.create(null);
obj.toString = function () {
return '成哥身体好'
}
document.write(obj); // 这里就会打印成哥身体好
// 因为document.write(obj) ==> document.write(obj.toString())
Float浮点类型
0.14 * 100 // 输出14.000000000000002 JavaScript精度不准的bug 无法解决 这是精度丢失 计算机保存的都是二进制的数,0.14不能用完整的二进制表示,导致最后的结果出现误差 所以尽量不要用浮点数进行判断
Math.ceil() // 向上取整
Math.floor() // 向下取整
Math.ceil(123.234) // 返回124 小数点后有值就会加1
Math.floor(123.234) // 返回123 小数点后的值不看
Math.random() // 随机生成0~1之间的数[0,1) 取不到1
for(var i = 0; i < 10; i ++) {
var num = Math.random().toFixed(2) * 100 // toFixed(2)是截取到第二位 这里是随机生成0~99的数 不含100
console.log(num); // 打印多个会发现精读不准的问题
}
// 所以先*100再取整 用floor向下取整 不用toFixed
for(var i = 0; i < 10; i ++) {
var num = Math.floor(Math.random() * 100) ;
console.log(num); // 这样打印就都是整数
}
// 可正常计算的范围:小数点前16位或者后16位
0.000000000000001 + 0.000000000000001 // 返回2e-15
// -15表示0后面总共有15个数,最后一位是2
0.000000000000000001 + 0.000000000000000001 // 返回2e-18
0.000000000000000011 + 0.000000000000000011 // 返回2.2e-18
// -18表示0后面总共有18个数,最后一位是2.2
// 十五位数
100000000000001 + 100000000000001 // 返回20000000000002
// 十六位数
1000000000000001 + 1000000000000001 // 返回20000000000000002
// 十七位数
10000000000000001 + 10000000000000001 //返回20000000000000000
// 十五位数
0.000000000000001 + 0.000000000000001 //返回0.00000000000002
// 十六位数
0.0000000000000001 + 0.0000000000000001 //返回0.00000000000000002
// 十七位数
0.00000000000000001 + 0.00000000000000001 //返回0.00000000000000000
call/apply
作用,改变this指向。
借用别人的函数实现自己的功能
function Person(name, age) { // this默认是指向window
this.name = name;
this.age = age
}
var person = new Person('deng', 100)
var obj = {
}
Person.call(obj,'cheng', 300); // call可以传东西 这里把obj传进去 会引导Person函数中的所有this都变成obj 然后实参传进去
// 访问obj打印的对象为 obj借别人的方法给自己增加了属性
Object {
name : 'cheng';
age : 300
}
/*
function test() {
}
test() --> test.call(); //函数执行隐藏的步骤
*/
function Person(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
// 另外一个方法也需要上面的属性,但是自身还有其他属性
function Student(name, age, sex, tel, grade) {
// var this = {} Person执行完之后这里面就多了name,age,sex属性
Person.call(this, name, age, sex) // 这里的this就是隐式的this对象 执行Person 把参数传进去 利用Person的方法实现自己的功能
// 然后再执行自己的两行
this.tel = tel;
this.grade = grade;
}
var student = new Student('sunny', 123, 'male', 139, 2017);
// 造车工厂 不同方法造不同部分
function Wheel(wheelSize, style) {
this.style = style;
this.wheelSize = wheelSize;
}
function Sit(comfortable, sitColor) {
this.comfortable = comfortable;
this.sitColor = sitColor;
}
function Model(height, width, len) {
this.height = height;
this.width = width;
this.len = len;
}
// 组装起来
function Car(wheelSize, style, comfortable, sitColor, height, width, len) {
Wheel.call(this, wheelSize, style);
Sit.call(this, comfortable, sitColor);
Model.call(this, height, width, len);
}
var car = new Car(100, '花里胡哨的', '真皮', 'red', 1800, 1900, 4900)
// 访问car对象 会发现car中所有属性都有
区别,后面传的参数形式不同。
call需要把实参按照形参的个数传进去
apply需要传一个arguments(数组) 且数组中包含实参
// 造车工厂 不同方法造不同部分
function Wheel(wheelSize, style) {
this.style = style;
this.wheelSize = wheelSize;
}
function Sit(comfortable, sitColor) {
this.comfortable = comfortable;
this.sitColor = sitColor;
}
function Model(height, width, len) {
this.height = height;
this.width = width;
this.len = len;
}
// 组装起来
function Car(wheelSize, style, comfortable, sitColor, height, width, len) {
Wheel.apply(this, [wheelSize, style]); // call是一个一个的传实参 apply必须传数组 数组里面包含实参
Sit.apply(this, [comfortable, sitColor]);
Model.apply(this, [height, width, len]);
}
var car = new Car(100, '花里胡哨的', '真皮', 'red', 1800, 1900, 4900)
// 访问car对象 会发现car中所有属性都有
练习
// JavaScript的call和apply方法是做什么的,两者有什么区别?
作用:改变this指向
区别:传参列表不同
arguments是js中函数内置的一个对象,而执行函数方法的实参中值都存储在arguments中
// 下面代码中console.log的结果是[1,2,3,4,5]的选项的是:A、C、D
// A
function foo(x) {
console.log(arguments)
return x
}
foo(1,2,3,4,5)
// B 没有函数调用 不报错也不执行
function foo(x) {
console.log(arguments)
return x
}(1,2,3,4,5)
// C 立即执行函数
(function foo(x) {
console.log(arguments)
return x
})(1,2,3,4,5)
// D
function foo(){bar.apply(null,arguments)} // 执行bar中的打印
function bar(x){console.log(arguments)}
foo(1,2,3,4,5)
练习
// JavaScript的call和apply方法是做什么的,两者有什么区别?
作用:改变this指向
区别:传参列表不同
arguments是js中函数内置的一个对象,而执行函数方法的实参中值都存储在arguments中
// 下面代码中console.log的结果是[1,2,3,4,5]的选项的是:A、C、D
// A
function foo(x) {
console.log(arguments)
return x
}
foo(1,2,3,4,5)
// B 没有函数调用 不报错也不执行
function foo(x) {
console.log(arguments)
return x
}(1,2,3,4,5)
// C 立即执行函数
(function foo(x) {
console.log(arguments)
return x
})(1,2,3,4,5)
// D
function foo(){bar.apply(null,arguments)} // 执行bar中的打印
function bar(x){console.log(arguments)}
foo(1,2,3,4,5)