Data binding ของ JavaScript และการใช้งาน “this” keyword

NSLog0
Algorithml
Published in
4 min readFeb 1, 2020

โพสเก่าจาก Blog เดิม มีนาคม 29, 2016

บทความนี้จะมาอธิบายการทำงานของ “this” ปกติ “this” เราจะอ้างถึง properties ของคลาสนั้นๆ แต่ JavaScript แต่ต่างออกไปนิดหน่อยและการใช้ “this” นั้นทำได้หลายหลายวิธีมากขึ้นอยู่กับการ binding Properties ของเราครับ วันนี้จะมาสอนการใช้ “this” ให้ถูกวิธีกัน

ขอยกตัวอย่างภาษา Java ก่อนนะเพื่อให้เห็นว่า “this” ทำงานยังไง

public class Rectangle {
private int x, y;
private int width, height;
public Rectangle() {
this(0, 0, 1, 1);
}
public Rectangle(int width, int height) {
this(0, 0, width, height);
}
public Rectangle(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
}

การใช้ “this” ใน Java นั้นจะเห็นว่าเราจะว่าเป็นการอ้างถึง properties ในคลาส ซึ่งเราจะไม่สามารถทำการเรียก int x , y , width , height จากที่อื่นได้นอกจากเราจะสร้าง Method เพื่อรับและส่งค่า แล้ว JavaScript ละ ?

ในการ Binding ของ JavaScript นั้นจะมีอยู่ 4 แบบหลักๆ

  • Window Binding / Default Binding
  • Implicit Binding
  • Explicit Binding
  • New Binding

Window Binding / Default Binding

มันก็คือการที่เราใช้ “this” เพื่อไปเรียกอ๊อฟเจ๊ตหรือตัวแปรของ window ในบราวเซอร์นั้นเอง ในโค้ดผมเขียนฟังก์ชันปกติทั่วไป ดูตัวอย่าง

function foo() {
console.log(this.a);
}
var a = 2;foo(); // 2

แต่สิ่งที่ได้กลับไปเรียก var a ที่อยู่ข้างนอกออกมาให้เรา เพราะมันไปเรียกคำสั่งนี้มาทำงานแทนครับ window.a และ “this” ที่อ้างถึงก็ไม่ใช้อะไรอื่นแต่เป็น window นั้นเองครับ อธิบายเพิ่มเติมก็คือตัวแปรต่างที่เราปรกาศมาอย่าง var a จะได้รับ scope เป็น global ซึ่งจะเก็บไว้ใน window ครับ เหมือนกับเราประกาศ window.a = 2 แล้ว window คืออะไรมันคือ Object ที่ได้รับมาจากบราวเซอร์อีกทีนึงครับ

ถ้าเราไม่ต้องการให้มันไปเรียก window มาละ เรามีวิธีแก้ครับ คือใช้ strict mode แบบนี้

function foo() {
"use strict";
console.log(this.a);
}
var a = 2;foo(); // TypeError: `this` is `undefined

ฟังก์ชันของเราก็จะไม่ไปเรียก window มาทำงานแล้ว และก็จะได้ Error กลับมาเพราะมันไม่รู้จัก this.a

Implicit Binding

อธิบายสั่นๆ คือการอ้างไปถึง Object หรือ Properties ที่อยู่ใน Context ของตัวมันเองครับ

var person = {
firstName: 'John',
lastName: 'Doe',
sayHi: function() {
console.log('Hello ' + this.firstName + ' ' + this.lastName);
}
}
person.sayHi(); //Hello John Doe

หรือจะเขียนอีกแบบ

function hi(ctx) {
ctx.sayHi = function() {
console.log('Hello ' + this.firstName + ' ' + this.lastName);
}
}
var person1 = {
firstName: 'John',
lastName: 'Doe'
}
var person2 = {
firstName: 'John',
lastName: 'Roe'
}
hi(person1);
hi(person2);
person1.sayHi(); //Hello John Doe
person2.sayHi(); //Hello John Roe

หรืออีกตัวอย่างนึงซับซ้อนไปอีกนิดนึง

//[1]
function person() {
return {
name: 'John Doe',
print: function() {
console.log(this.name);
}
}
}
var obj = person('John Doe');
obj.print(); //John Doe
//[2]
function person(name, skill) {
return {
name: name,
print: function() {
console.log(this.name);
},
skill: {
name: skill,
print: function() {
console.log(this.name);
}
}
}
}
var obj = person('John Doe', 'Java, AngularJS, NodeJs');
obj.skill.print(); //Java, AngularJS, NodeJs

จะเห็นว่าในทุกๆ แบบเราสามารถใช้ “this” เรียกเอา Properties ของตัวมันเองแบบไม่มีปัญหาเลย ลองมาดูอีกแบบนึง

function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // function reference/alias!
var a = "oops, global"; // `a` also property on global object
bar(); // "oops, global"

กลับมาอีกแล้วครับ window ที่เป็นแบบนี้ก็เพราะว่า foo() มันประกาศอยู่นอก obj นั้นเองดังนั้น context มันเลยเปลี่ยนไปเป็น window ถึงแม้เราจะเรียกใน obj ก็ตาม แต่เราสามารถแก้ปัญหานี้ได้ครับ ลองมาดูหัวข้อถัดไป

Explicit Binding

ถ้าว่าง่ายๆ หลักการไม่เยอะมันคือการทำให้ฟังก์ชันของเราใช้ “this” ไปชี้ที่ใน scope ของมันเองครับ ไม่ชี้ที่ window หรือ context อื่นๆ นอกจากตัวมัน บางคนอาจจะเคยเห็นฟังก์ชันสามตัวนี้มาแล้ว call(), bind(), apply() เพราะการทำ explicit binding เราจะเอาสามฟังก์ชันที่ว่าเอามาใช้งาน ซึ่งฟังก์ชันพวกนี้จะผูกติดมากับฟังก์ชันทุกๆ ตัวที่เราสร้างบน JavaScript โดยปริยาย เราสามารถเรียกใช้ได้ตลอดเวลา

เพื่อแก้ปัญหาด้านบนเราจะลองมาเขียนแบบใหม่ดู

function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // function reference/alias!
var a = "oops, global"; // `a` also property on global object
bar.call(obj) //2

ผมใช้ call() แล้วโยน obj เข้าไปผลที่ได้มันก็จะไปเรียก obj.a ขึ้นมาให้เรา ซึ่งเราต้องการให้มันเป็นแบบนั้น และผมยังสามารถโยนตัวแปรหรืออ๊อฟเจ็คอื่นๆ เข้าไปได้อีกด้วยถ้าต้องการแบบนี้

//[1]
function print1() {
console.log(this.name);
}
//[2]
function print2(arg) {
console.log(this.name + ' ' + arg);
}
//[3]
function print3(arg1, arg2) {
console.log(this.name + ' ' + arg1 + ' ' + arg2);
}
var john = {
name: 'John Doe',
skill: 'Java, C'
}
print1.call(john); //John Doe
print2.call(john, 'age 24'); //John Doe age 24
print3.call(john, 'age 24', 'live in USA'); //John Doe age 24 live in USA

ซึ่งประโยชน์ของมันทำให้เรา reuse ฟังก์ชันได้สบายๆ และเรายังใส่ค่าลงไปกี่ตัวก็ได้เท่าที่เราต้องการ แต่ถ้าเรามีข้อมูลสัก 10 ตัวละ หรือมากกว่านั้น งั่นเราลองมาเปลี่ยนเป็นแบบนี้แทนดีกว่า

//[3]
function print3(arg1, arg2) {
console.log(this.name + ' ' + arg1 + ' ' + arg2);
}
var john = {
name: 'John Doe',
skill: 'Java, C'
}
var data = ['age 24', 'live in USA'];
print3.apply(john, data); //John Doe age 24 live in USA

ผมยกเอาส่วนของ print3 มาเขียนใหม่ โดยครั่งนี้เรียกผ่าน apply() แทนแล้วทำการโยนอาเรย์เข้าไปแทนการใส่ค่าทีละตัว ซึ่งก็ให้ผลไม่ต่างจาก call() แต่ต่างกันตรงที่ call() รับค่าที่ละตัวแบบไม่จำกัดจำนวน ส่วน apply() จะรับค่าที่เป็นอาเรย์เข้าไป

ฟังก์ชันสุดท้ายคือ bind() มาดูกันว่าเขียนยังไง

//[3]
function print3(arg1, arg2) {
console.log(this.name + ' ' + arg1 + ' ' + arg2);
}
var john = {
name: 'John Doe',
skill: 'Java, C'
}
var data = ['age 24', 'live in USA'];
var fn1 = print3.bind(john, data);
var fn2 = print3.bind(john, 'age 24', 'live in USA');
fn1(); //John Doe age 24 live in USA
fn2(); //John Doe age 24 live in USA

ผลที่ได้ก็เหมือนกับ call() และ apply() แต่ต่างกันตรงที่ call() และ apply() นั้นจะเรียกให้ฟังก์ชันทำงานทันที่หลังจากเราเรียกใช้งาน แต่ bind() นั้นจะทำการโยนค่าที่ส่งเข้าไปออกมาเป็นฟังก์ชันเพื่อรอให้เราเรียกใช้งานอีกทีนึงครับ

New Binding

ตัวนี้จะเป็นการทำงานกับ keyoword “new” หรือก็คือการสร้างอ๊อปเจ๊คนั้นเอง ถ้าใครเคยเขียน Java มาต้องร้อง อ่อ แน่ๆ เรามาดูตัวอย่างกัน โดยผมจะเอาโค้ด Java ข้างบนมาเขียนใหม่

public class Rectangle {
private int x, y;
private int width, height;
public Rectangle(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
public void print_me() {
System.out.print(this.x + " " + this.y + " " + this.width +
" " + this.height);
}
}
public class App {
public static void main(String[] args) {
Rectangle rec = new Rectangle(0, 0, 45, 45);
rec.print_me(); //0 0 45 45
}
}

จะเห็นว่าผมสร้างอ๊อฟเจ๊คจาก “new” ขึ้นมาจากนั้นก็เรียกใช้งานเมธธอด print_me() ให้ทำงาน ซึ่งใน JavaScript หลักการทำงานก็คล้ายๆ กัน แต่ JavaScript มันไม่มี Class ครับ ผมจะลองเอาโค้ด Java ข้างบนมาแปลงใหม่แบบนี้ ให้เป็น JavaScript

function Rectangle(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
Rectangle.prototype = {
print_me: function() {
console.log(this.x + " " + this.y + " " + this.width + " " +
this.height);
}
}
var rec = new Rectangle(0, 0, 45, 45);rec.print_me(); //0 0 45 45

จะเห็นว่าโค้ด JavaScript ผมได้แปลงมาจาก Java ข้างบน ตรงส่วนของ print_me ผมได้ใช้ prototype เพื่อทำการ inheritance ตัว Rectangle อีกทีนึงครับ เราจะลองมาดูตัวอย่างสุดท้ายการโดยนำเอา Explicit Binding มารวมด้วยแบบนี้

Rectangle.prototype = {
print_me: function() {
console.log(this.x + " " + this.y + " " + this.width + " " +
this.height);
},
calcu: function(num) {
console.log(this.x + this.y + this.width + this.height + num);
}
}
var rec = new Rectangle(0, 0, 45, 45);
rec.print_me(); //0 0 45 45
//[1]
rec.calcu.call(rec, 3); //93
//[2]
rec.calcu.apply(rec, [5]); //95
//[3]
var fn = rec.calcu.bind(rec, 7);
fn(); //97

ผมได้เพิ่ม calcu() เข้าไปอีก โดยผมยกโค้ดจากตัวอย่างแรกมาให้ เห็นไหมครับ เราสามารถเอามันมาประยุกต์ใช่ได้หลายแบบเลยทีเดียว **ข้อควรจำ การจะใช้ “new” ได้ต้องไม่มีการ return ค่าใดๆ ทั้งสิ้น หรือหมายความว่าต้องเขียนด้วย “this” ทั้งหมดครับ

สำหรับบทความนี้มือใหม่อาจจะ งง แต่ผมเชื่อว่าไม่น่ายากเกินไป อ่านเสร็จแล้วลองไปอ่านเพิ่มเติมและลองทำตามเพื่อความเข้าใจนะครับ

--

--

NSLog0
Algorithml

I’m a Software Developer and Underwater photographer